# -*- coding: UTF-8 -*-
from django.core import formfields, validators
from django.core import db
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
import copy, datetime, os, re, sys, types

# Les valeurs à utiliser au sein des champs SelectField à la place des valeurs vierges. Ces valeurs seront ajoutées au début de la plupart des listes présentant plusieurs choix possibles.
BLANK_CHOICE_DASH = [("", "---------")]
BLANK_CHOICE_NONE = [("", "Aucun")]

# Les différentes phases de l'interface d'administration.
ADD, CHANGE, BOTH = 1, 2, 3

# Les valeurs que peut prendre Relation.edit_inline_type.
TABULAR, STACKED = 1, 2

# Les valeurs que peut prendre filter_interface.
HORIZONTAL, VERTICAL = 1, 2

# Une chaîne aléatoire que l'on utilise pour les paramètres "default".
NOT_PROVIDED = 'oijpwojefiojpanv'

# La taille du "morceau" lu à chaque appel de get_iterator.
# Plus cette valeur est grande, plus la vitesse est accrue et l'espace de stockage nécessaire important.
GET_ITERATOR_CHUNK_SIZE = 100

# Préfixe (style "chemin d'accès Python") qui indique l'emplacement des modèles de données.
MODEL_PREFIX = 'django.models'

# Les méthodes d'un modèle de données dont le nom commence par le préfixe suivant
# seront supprimées du modèle de données et converties en fonctions au niveau du module.
MODEL_FUNCTIONS_PREFIX = '_module_'

# Les méthodes d'un modèle de données dont le nom commence par le préfixe suivant
# seront supprimées du modèle de données et converties en méthodes d'un manipulateur.
MANIPULATOR_FUNCTIONS_PREFIX = '_manipulator_'

LOOKUP_SEPARATOR = '__'

RECURSIVE_RELATIONSHIP_CONSTANT = 'self'

#########################
# FONCTIONS ASSISTANTES #
#########################

# met en majuscule la première lettre de la chaîne spécifiée
capfirst = lambda x: x and x[0].upper() + x[1:]

# prépare une valeur de manière à ce qu'elle puisse être incluse au sein d'une requête LIKE
prep_for_like_query = lambda x: str(x).replace("%", "\%").replace("_", "\_")

# renvoie la classe CSS <ul> correspondant à la valeur radio_admin spécifiée
get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')

def curry(*args, **kwargs):
    def _curried(*moreargs, **morekwargs):
        return args[0](*(args[1:]+moreargs), **dict(kwargs.items() + morekwargs.items()))
    return _curried

def get_module(app_label, module_name):
    return __import__('%s.%s.%s' % (MODEL_PREFIX, app_label, module_name), '', '', [''])

def get_app(app_label):
    return __import__('%s.%s' % (MODEL_PREFIX, app_label), '', '', [''])

_installed_models_cache = None
def get_installed_models():
    """
    Renvoie une liste des paquets contenant des "modèles de données" installés; cette liste
    contient donc des paquets tels que toto.models, ellington.news.models etc. Cette liste
    NE CONTIENT PAS django.models.
    """
    global _installed_models_cache
    if _installed_models_cache is not None:
        return _installed_models_cache
    _installed_models_cache = []
    for a in settings.INSTALLED_APPS:
        try:
            _installed_models_cache.append(__import__(a + '.models', '', '', ['']))
        except ImportError:
            pass
    return _installed_models_cache

_installed_modules_cache = None
def get_installed_model_modules(core_models=None):
    """
    Renvoie une liste des modèles de données installés; cette liste
    contient donc des modèles de données tels que django.models.core,
    ellington.news.models.news, toto.models.zozo etc.
    """
    global _installed_modules_cache
    if _installed_modules_cache is not None:
        return _installed_modules_cache
    _installed_modules_cache = []

    # django.models est un cas particulier.
    for submodule in (core_models or []):
        _installed_modules_cache.append(__import__('django.models.%s' % submodule, '', '', ['']))
    for m in get_installed_models():
        for submodule in getattr(m, '__all__', []):
            _installed_modules_cache.append(__import__('django.models.%s' % submodule, '', '', ['']))
    return _installed_modules_cache

class LazyDate:
    """
    Combinez cette classe avec limit_choices_to pour sélectionner une gamme d'objets
    en fonction de dates calculées non au moment du chargement du modèle de données
    mais au moment de l'exécution. À titre d'exemple :

    ::

        ... limit_choices_to = {'date__gt' : meta.LazyDate(days=-3)} ...

    limitera la gamme de choix possibles aux objets datant de plus de trois jours
    pour le champ correspondant.
    """
    def __init__(self, **kwargs):
        self.delta = datetime.timedelta(**kwargs)

    def __str__(self):
        return str(self.__get_value__())

    def __repr__(self):
        return "<Objet LazyDate : %s>" % self.delta

    def __get_value__(self):
        return datetime.datetime.now() + self.delta

#######################
# CLASSES PRINCIPALES #
#######################

class FieldDoesNotExist(Exception):
    pass

class BadKeywordArguments(Exception):
    pass

class Options:
    def __init__(self, module_name='', verbose_name='', verbose_name_plural='', db_table='',
        fields=None, ordering=None, unique_together=None, admin=None, has_related_links=False,
        where_constraints=None, object_name=None, app_label=None,
        exceptions=None, permissions=None, get_latest_by=None,
        order_with_respect_to=None, module_constants=None):

        # On enregistre les arguments passés originalement à la fonction; ils
        # serviront à la méthode copy(). Notez que nous N'UTILISONS PAS
        # copy.deepcopy() car cela aurait pour effet de créer en mémoire une
        # nouvelle copie de chaque argument, et il est toujours préférable de
        # préserver l'espace mémoire. Bien sûr, cela a pour nous une conséquence
        # importante : modifier un attribut d'un objet Options aura pour effet
        # de modifier sa valeur dans self._orig_init_args, il faut donc que nous
        # fassions attention et que nous évitions de faire cela. En pratique, on
        # y arrive très bien car les objets Options sont généralement des objets
        # en lecture seule et __init__() constitue le seul endroit où les
        # attributs d'un objet Options sont manipulés.

        # L'appel locals() est une simple commodité, qui nous permet d'éviter
        # d'avoir à écrire quelque chose de plus long comme :
        #    self._orig_init_args = {
        #       'module_name': module_name,
        #       'verbose_name': verbose_name,
        #       ...
        #    }
        self._orig_init_args = locals()
        del self._orig_init_args['self'] # parce qu'on s'en fiche.

        # On place les champs de self.fields impliqués dans une relation plusieurs-plusieurs
        # au sein de self.many_to_many.
        self.fields, self.many_to_many = [], []
        for field in (fields or []):
            if field.rel and isinstance(field.rel, ManyToMany):
                self.many_to_many.append(field)
            else:
                self.fields.append(field)
        self.module_name, self.verbose_name = module_name, verbose_name
        self.verbose_name_plural = verbose_name_plural or verbose_name + 's'
        self.db_table, self.has_related_links = db_table, has_related_links
        self.ordering = ordering or []
        self.unique_together = unique_together or []
        self.where_constraints = where_constraints or []
        self.exceptions = exceptions or []
        self.permissions = permissions or []
        self.object_name, self.app_label = object_name, app_label
        self.get_latest_by = get_latest_by
        if order_with_respect_to:
            self.order_with_respect_to = self.get_field(order_with_respect_to)
            self.ordering = (('_order', 'ASC'),)
        else:
            self.order_with_respect_to = None
        self.module_constants = module_constants or {}
        self.admin = admin

        # On calcule one_to_one_field.
        self.one_to_one_field = None
        for f in self.fields:
            if isinstance(f.rel, OneToOne):
                self.one_to_one_field = f
                break
        # On met en cache le champ faisant office de clé primaire.
        self.pk = None
        for f in self.fields:
            if f.primary_key:
                self.pk = f
                break
        # Si un champ faisant office de clé primaire n'a pas été spécifié, on
        # ajoute un champ ID faisant office de clé primaire; ce champ
        # s'incrémente automatiquement.
        if self.pk is None:
            self.fields.insert(0, AutoField('id', 'identifiant', primary_key=True))
            self.pk = self.fields[0]

    def __repr__(self):
        return '<Options associées au module %s>' % self.module_name

    def copy(self, **kwargs):
        args = self._orig_init_args.copy()
        args.update(kwargs)
        return self.__class__(**args)

    def get_model_module(self):
        return get_module(self.app_label, self.module_name)

    def get_content_type_id(self):
        "Renvoie l'identifiant de l'objet content-type correspondant au type de l'objet actuel."
        if not hasattr(self, '_content_type_id'):
            mod = get_module('core', 'contenttypes')
            self._content_type_id = mod.get_object(python_module_name__exact=self.module_name, package__label__exact=self.app_label).id
        return self._content_type_id

    def get_field(self, name, many_to_many=True):
        """
        Renvoie le champ dont le nom est spécifié. Déclenche l'exception FieldDoesNotExist en cas d'erreur.
        """
        to_search = many_to_many and (self.fields + self.many_to_many) or self.fields
        for f in to_search:
            if f.name == name:
                return f
        raise FieldDoesNotExist, "nom=%s" % name

    def get_order_sql(self, table_prefix=''):
        "Renvoie la clause 'ORDER BY' complète correspondant à l'objet actuel, générée grâce à l'attribut self.ordering."
        if not self.ordering: return ''
        pre = table_prefix and (table_prefix + '.') or ''
        return 'ORDER BY ' + ','.join(['%s%s %s' % (pre, f, order) for f, order in self.ordering])

    def get_add_permission(self):
        return 'add_%s' % self.object_name.lower()

    def get_change_permission(self):
        return 'change_%s' % self.object_name.lower()

    def get_delete_permission(self):
        return 'delete_%s' % self.object_name.lower()

    def get_rel_object_method_name(self, rel_opts, rel_field):
        # Cette méthode intègre l'algorithme qui décide du nom à donner aux
        # méthodes chargées d'obtenir auprès de la base de données des objets
        # situés à l'autre bout d'une relation plusieurs-un. Généralement,
        # l'algorithme se contente d'utiliser l'option object_name en
        # minuscules, mais si l'objet lié par la relation se trouve dans une
        # autre application, l'attribut app_label de cette application est
        # ajouté au début du nom.
        #
        # Exemples :
        #
        #   # Le cas classique : l'objet lié par la relation se trouve dans la même application.
        #   # Cette méthode renvoie "reponse".
        #   Sondage.get_reponse_list()
        #
        #   # L'objet lié par la relation se trouve dans une application différente.
        #   # Cette méthode renvoie "lcom_bestof".
        #   Lieu.get_lcom_bestof_list() # "lcom_bestof"
        rel_obj_name = rel_field.rel.related_name or rel_opts.object_name.lower()
        if self.app_label != rel_opts.app_label:
            rel_obj_name = '%s_%s' % (rel_opts.app_label, rel_obj_name)
        return rel_obj_name

    def get_all_related_objects(self):
        try: # On essaie d'abord de renvoyer le cache.
            return self._all_related_objects
        except AttributeError:
            module_list = get_installed_model_modules()
            rel_objs = []
            for mod in module_list:
                for klass in mod._MODELS:
                    for f in klass._meta.fields:
                        if f.rel and self == f.rel.to:
                            rel_objs.append((klass._meta, f))
            if self.has_related_links:
                # On ajoute à la main les objets RelatedLink, qui constituent des cas particuliers.
                core = get_module('relatedlinks', 'relatedlinks')
                # Notez que l'emploi de copy() est très important; sans copy(), tout
                # objet qui serait chargé par la suite et qui possèderait des liens
                # connexes écraserait la relation que nous créons aux lignes suivantes.
                link_field = copy.copy(core.RelatedLink._meta.get_field('object_id'))
                link_field.rel = ManyToOne(self.get_model_module().Klass, 'related_links', 'id',
                    num_in_admin=3, min_num_in_admin=3, edit_inline=True, edit_inline_type=TABULAR,
                    lookup_overrides={
                        'content_type__package__label__exact': self.app_label,
                        'content_type__python_module_name__exact': self.module_name
                    })
                rel_objs.append((core.RelatedLink._meta, link_field))
            self._all_related_objects = rel_objs
            return rel_objs

    def get_inline_related_objects(self):
        return [(a, b) for a, b in self.get_all_related_objects() if b.rel.edit_inline]

    def get_all_related_many_to_many_objects(self):
        module_list = get_installed_model_modules()
        rel_objs = []
        for mod in module_list:
            for klass in mod._MODELS:
                try:
                    for f in klass._meta.many_to_many:
                        if f.rel and self == f.rel.to:
                            rel_objs.append((klass._meta, f))
                            raise StopIteration
                except StopIteration:
                    continue
        return rel_objs

    def get_ordered_objects(self):
        "Renvoie une liste des objets Options correspondant aux objets qui doivent être triés en fonction de l'objet actuel."
        if not hasattr(self, '_ordered_objects'):
            objects = []
            for klass in get_app(self.app_label)._MODELS:
                opts = klass._meta
                if opts.order_with_respect_to and opts.order_with_respect_to.rel \
                    and self == opts.order_with_respect_to.rel.to:
                    objects.append(opts)
            self._ordered_objects = objects
        return self._ordered_objects

    def has_field_type(self, field_type):
        """
        Renvoie True si le formulaire d'administration correspondant à l'objet
        actuel possède au moins un champ du type spécifié en argument
        (FileField est un exemple de type de champ).
        """
        if not hasattr(self, '_field_types'):
            self._field_types = {}
        if not self._field_types.has_key(field_type):
            try:
                # On regarde d'abord dans self.fields.
                for f in self.fields:
                    if isinstance(f, field_type):
                        raise StopIteration
                # Si cela échoue, on regarde dans les champs connexes.
                for rel_obj, rel_field in self.get_inline_related_objects():
                    for f in rel_obj.fields:
                        if isinstance(f, field_type):
                            raise StopIteration
            except StopIteration:
                self._field_types[field_type] = True
            else:
                self._field_types[field_type] = False
        return self._field_types[field_type]

def _reassign_globals(function_dict, extra_globals, namespace):
    new_functions = {}
    for k, v in function_dict.items():
        # On obtient l'objet-code correspondant à la fonction.
        code = v.func_code
        # On recrée la fonction mais on s'arrange pour lui donner accès aux
        # variables globales spécifiées par l'argument extra_globals ainsi
        # qu'aux variables globales de l'espace de noms spécifié.
        new_globals = {'__builtins__': __builtins__, 'db': db.db, 'datetime': datetime}
        new_globals.update(extra_globals.__dict__)
        func = types.FunctionType(code, globals=new_globals, name=k, argdefs=v.func_defaults)
        func.__dict__.update(v.__dict__)
        setattr(namespace, k, func)
        # On donne à toutes les fonctions personnalisées que nous avons ajoutées
        # jusqu'ici accès à la nouvelle fonction que nous venons de créer.
        for new_k, new_v in new_functions.items():
            new_v.func_globals[k] = func
        new_functions[k] = func

class ModelBase(type):
    "La méta-classe de tous les modèles de données"
    def __new__(cls, name, bases, attrs):
        # Si la classe actuelle n'est pas une sous-classe de la classe Model, on ne fait rien de spécial.
        if not bases:
            return type.__new__(cls, name, bases, attrs)

        # Si le modèle de données actuel est une sous-classe d'un autre modèle
        # de données (lui-même une sous-classe de la classe Model), on crée
        # l'objet Options de la manière suivante : on copie d'abord l'attribut
        # _meta de la classe de base, puis on le met à jour en prenant en
        # compte les valeurs que la classe actuelle redéfinit.
        replaces_module = None
        if bases[0] != Model:
            if not attrs.has_key('fields'):
                attrs['fields'] = list(bases[0]._meta._orig_init_args['fields'][:])
            if attrs.has_key('ignore_fields'):
                ignore_fields = attrs.pop('ignore_fields')
                new_fields = []
                for i, f in enumerate(attrs['fields']):
                    if f.name not in ignore_fields:
                        new_fields.append(f)
                attrs['fields'] = new_fields
            if attrs.has_key('add_fields'):
                attrs['fields'].extend(attrs.pop('add_fields'))
            if attrs.has_key('replaces_module'):
                # Pour l'instant, on ne fait que définir la variable
                # replaces_module. On ne peut en réalité encore rien faire
                # avec cette variable, car le module correspondant n'a pas
                # encore été créé.
                replaces_module = attrs.pop('replaces_module').split('.')
            # On propage chaque valeur que la classe actuelle redéfinit au sein
            # de l'objet Options de la classe de base et on retire en même temps
            # ces valeurs de la variable attrs. Une fois cette opération terminée,
            # le dictionnaire attrs ne contient plus que des noms de méthodes
            # personnalisées et __module__.
            meta_overrides = {}
            for k, v in attrs.items():
                if not callable(v) and k != '__module__':
                    meta_overrides[k] = attrs.pop(k)
            opts = bases[0]._meta.copy(**meta_overrides)
            opts.object_name = name
            del meta_overrides
        else:
            opts = Options(
                # Si l'option module_name n'a pas été spécifiée, on utilise
                # le nom de la classe actuelle en minuscules et on lui rajoute
                # un "s" final (une "mise au pluriel" simplifiée).
                module_name = attrs.pop('module_name', name.lower() + 's'),
                # Si l'option verbose_name n'a pas été spécifiée, on utilise
                # le nom de la classe actuelle, transformé de la manière
                # suivante : MajusculesAuDebutDesMots devient "majuscules au debut
                # des mots".
                verbose_name = attrs.pop('verbose_name',
                    re.sub('([A-Z])', ' \\1', name).lower().strip()),
                verbose_name_plural = attrs.pop('verbose_name_plural', ''),
                db_table = attrs.pop('db_table', ''),
                fields = attrs.pop('fields'),
                ordering = attrs.pop('ordering', None),
                unique_together = attrs.pop('unique_together', None),
                admin = attrs.pop('admin', None),
                has_related_links = attrs.pop('has_related_links', False),
                where_constraints = attrs.pop('where_constraints', None),
                object_name = name,
                app_label = attrs.pop('app_label', None),
                exceptions = attrs.pop('exceptions', None),
                permissions = attrs.pop('permissions', None),
                get_latest_by = attrs.pop('get_latest_by', None),
                order_with_respect_to = attrs.pop('order_with_respect_to', None),
                module_constants = attrs.pop('module_constants', None),
            )

        # On crée dynamiquement le module qui contiendra cette classe et ses
        # fonctions assistantes associées.
        if replaces_module is not None:
            new_mod = get_module(*replaces_module)
        else:
            new_mod = types.ModuleType(opts.module_name)

        # On rassemble toutes les méthodes personnalisées, s'il y en a, ainsi que toutes les
        # fonctions personnalisées du module, s'il y en a, et on les place temporairement
        # dans des variables. On s'occupera d'elles plus tard.
        if replaces_module is not None:
            # On initialise custom_methods et custom_functions en leur affectant
            # les valeurs que ces deux variables prennent au sein de la classe de base.
            custom_methods = dict([(k, v) for k, v in new_mod.Klass.__dict__.items() if hasattr(v, 'custom')])
            custom_functions = dict([(k, v) for k, v in new_mod.__dict__.items() if hasattr(v, 'custom')])
        else:
            custom_methods, custom_functions = {}, {}
        manipulator_methods = {}
        for k, v in attrs.items():
            if k in ('__module__', '__init__', '_overrides', '__doc__'):
                continue # On saute tout ce qui pourrait être important.
            # On affecte à la fonction un attribut spécial "custom", qui désigne
            # que c'est une fonction/méthode personnalisée.
            v.custom = True
            if k.startswith(MODEL_FUNCTIONS_PREFIX):
                custom_functions[k[len(MODEL_FUNCTIONS_PREFIX):]] = v
            elif k.startswith(MANIPULATOR_FUNCTIONS_PREFIX):
                manipulator_methods[k[len(MANIPULATOR_FUNCTIONS_PREFIX):]] = v
            else:
                custom_methods[k] = v
            del attrs[k]

        # On crée au niveau du module l'exception ObjetDoesNotExist.
        dne_exc_name = '%sDoesNotExist' % name
        does_not_exist_exception = types.ClassType(dne_exc_name, (ObjectDoesNotExist,), {})
        # On définit explicitement l'attribut __module__ de cette exception car il
        # aura pour valeur initiale le nom du module dans lequel le code précédent
        # a été exécuté (ce qui est incorrect).
        does_not_exist_exception.__module__ = MODEL_PREFIX + '.' + opts.module_name
        setattr(new_mod, dne_exc_name, does_not_exist_exception)

        # On crée les autres exceptions.
        for exception_name in opts.exceptions:
            exc = types.ClassType(exception_name, (Exception,), {})
            exc.__module__ = MODEL_PREFIX + '.' + opts.module_name # On définit __module__ explicitement, comme ci-dessus.
            setattr(new_mod, exception_name, exc)

        # On crée au niveau du module toutes les constantes nécessaires, s'il y en a.
        for k, v in opts.module_constants.items():
            setattr(new_mod, k, v)

        # On crée les méthodes par défaut de la classe.
        attrs['__init__'] = curry(method_init, opts)
        attrs['__eq__'] = curry(method_eq, opts)
        attrs['save'] = curry(method_save, opts)
        attrs['save'].alters_data = True
        attrs['delete'] = curry(method_delete, opts)
        attrs['delete'].alters_data = True

        if opts.order_with_respect_to:
            attrs['get_next_in_order'] = curry(method_get_next_in_order, opts, opts.order_with_respect_to)
            attrs['get_previous_in_order'] = curry(method_get_previous_in_order, opts, opts.order_with_respect_to)

        for f in opts.fields:
            # Si l'objet est impliqué dans une relation avec lui-même, ce qui nous est
            # indiqué par la constante RECURSIVE_RELATIONSHIP_CONSTANT, on crée de
            # manière formelle cette relation.
            if f.rel and f.rel.to == RECURSIVE_RELATIONSHIP_CONSTANT:
                f.rel.to = opts
                f.name = (f.rel.name or f.rel.to.object_name.lower()) + '_' + f.rel.to.pk.name
                f.verbose_name = f.verbose_name or f.rel.to.verbose_name
                f.rel.field_name = f.rel.field_name or f.rel.to.pk.name
            # On ajoute des méthodes "get_truc" aux objets impliqués dans une relation plusieurs-un.
            # EXEMPLES : Choix.get_sondage(), Article.get_ligne_date_lieu()
            if isinstance(f.rel, ManyToOne):
                func = curry(method_get_many_to_one, f)
                func.__doc__ = "Renvoie l'objet `%s.%s` associé." % (f.rel.to.app_label, f.rel.to.module_name)
                attrs['get_%s' % f.rel.name] = func

        for f in opts.many_to_many:
            # On ajoute des méthodes "get_truc" aux objets impliqués dans une relation plusieurs-plusieurs.
            # EXEMPLES : Sondage.get_site_list(), Article.get_ligne_auteur_list()
            func = curry(method_get_many_to_many, f)
            func.__doc__ = "Renvoie une liste des objets `%s.%s` associés." % (f.rel.to.app_label, f.rel.to.module_name)
            attrs['get_%s_list' % f.rel.name] = func
            # On ajoute des méthodes "set_truc" aux objets impliqués dans une relation plusieurs-plusieurs.
            # EXEMPLES : Sondage.set_sites(), Article.set_lignes_auteur()
            func = curry(method_set_many_to_many, f)
            func.__doc__ = "Affecte à la liste d'objets `%s.%s` de cet objet la liste d'identifiants spécifiée. Notez que cette méthode ne vérifie pas si les identifiants spécifiés sont valides." % (f.rel.to.app_label, f.rel.to.module_name)
            func.alters_data = True
            attrs['set_%s' % f.name] = func

        # On crée la classe, car on en a besoin pour la curryfication.
        new_class = type.__new__(cls, name, bases, attrs)

        # On donne à la classe une chaîne de documentation (sa définition, en fait).
        new_class.__doc__ = "%s.%s(%s)" % (opts.module_name, name, ", ".join([f.name for f in opts.fields]))

        # On crée au niveau du module les fonctions assistantes standard de l'API, telles que
        # get_object() et get_list().
        new_mod.get_object = curry(function_get_object, opts, new_class, does_not_exist_exception)
        new_mod.get_object.__doc__ = "Renvoie l'objet %s qui correspond aux paramètres spécifiés." % name

        new_mod.get_list = curry(function_get_list, opts, new_class)
        new_mod.get_list.__doc__ = "Renvoie une liste des objets %s qui correspondent aux paramètres spécifiés." % name

        new_mod.get_iterator = curry(function_get_iterator, opts, new_class)
        new_mod.get_iterator.__doc__ = "Renvoie un itérateur qui génère les objets %s correspondant aux paramètres spécifiés." % name

        new_mod.get_count = curry(function_get_count, opts)
        new_mod.get_count.__doc__ = "Renvoie le nombre d'objets %s qui correspondent aux paramètres spécifiés." % name

        new_mod._get_sql_clause = curry(function_get_sql_clause, opts)

        new_mod.get_in_bulk = curry(function_get_in_bulk, opts, new_class)
        new_mod.get_in_bulk.__doc__ = "Renvoie un dictionnaire d'entrées du style 'ID': <objet %s>; chaque entrée correspond à l'objet %s dont l'identifiant a été spécifié au sein de l'argument id_list." % (name, name)

        if opts.get_latest_by:
            new_mod.get_latest = curry(function_get_latest, opts, new_class, does_not_exist_exception)

        for f in opts.fields:
            if isinstance(f, DateField) or isinstance(f, DateTimeField):
                # On ajoute des méthodes "get_next_by_truc" et "get_previous_by_truc"
                # pour chaque champ DateField ou DateTimeField ne pouvant pas être
                # laissé vierge.
                # EXEMPLES : Sondage.get_next_by_date_pub(), Sondage.get_previous_by_date_pub()
                if not f.null:
                    setattr(new_class, 'get_next_by_%s' % f.name, curry(method_get_next_or_previous, new_mod.get_object, f, True))
                    setattr(new_class, 'get_previous_by_%s' % f.name, curry(method_get_next_or_previous, new_mod.get_object, f, False))
                # On ajoute une méthode "get_truc_list" pour chaque champ DateField ou
                # DateTimeField.
                # EXEMPLE : sondages.get_date_pub_list()
                func = curry(function_get_date_list, opts, f)
                func.__doc__ = "Renvoie une liste des jours, mois ou années (sous forme d'objets datetime.datetime) pour lesquels des objets %s sont disponibles. Le premier paramètre (dont le nom est 'kind') peut prendre la valeur 'year' (pour année), 'month' (pour mois) ou 'day' (pour jour)." % name
                setattr(new_mod, 'get_%s_list' % f.name, func)

            elif isinstance(f, FileField):
                setattr(new_class, 'get_%s_filename' % f.name, curry(method_get_file_filename, f))
                setattr(new_class, 'get_%s_url' % f.name, curry(method_get_file_url, f))
                setattr(new_class, 'get_%s_size' % f.name, curry(method_get_file_size, f))
                func = curry(method_save_file, f)
                func.alters_data = True
                setattr(new_class, 'save_%s_file' % f.name, func)
                if isinstance(f, ImageField):
                    # On ajoute des méthodes get_TRUC_width et get_TRUC_height,
                    # mais seulement si le champ ImageField ne possède pas
                    # de champs chargés de contenir la longueur et la hauteur
                    # de l'image.
                    if not f.width_field:
                        setattr(new_class, 'get_%s_width' % f.name, curry(method_get_image_width, f))
                    if not f.height_field:
                        setattr(new_class, 'get_%s_height' % f.name, curry(method_get_image_height, f))

        # On ajoute au module que nous venons de créer la classe elle-même.
        new_mod.__dict__[name] = new_class

        # On ajoute la variable "Klass" : un raccourci qui fait référence à la classe créée plus haut.
        new_mod.__dict__['Klass'] = new_class

        # On ajoute les manipulateurs.
        new_mod.__dict__['AddManipulator'] = get_manipulator(opts, new_class, manipulator_methods, add=True)
        new_mod.__dict__['ChangeManipulator'] = get_manipulator(opts, new_class, manipulator_methods, change=True)

        # Maintenant que nous disposons de références au module (new_mod) et à
        # la classe (new_class), on peut ajouter (s'il y a lieu) toutes les
        # méthodes personnalisées à la nouvelle classe. Notez qu'on aurait très
        # bien pu garder inchangées les méthodes supplémentaires dans attrs (ce
        # que l'on a pas fait, voir ci-dessus), mais cela aurait été fâcheux
        # pour elles; en effet, elles *n'auraient alors pas eu* accès aux éléments
        # globaux définis au niveau du module, comme la fonction get_list(),
        # la variable db etc. Afin de rendre ces éléments globaux accessibles
        # à ces méthodes personnalisées, on est obligé de les reconstruire à la
        # main : ainsi, pour chaque méthode personnalisée, on obtient son
        # objet-code brut, et on la recrée en lui associant un nouveau dictionnaire
        # "globals" (qui contient les éléments globaux souhaités).
        #
        # Pour compliquer un peu plus les choses, comme chaque méthode se voit
        # affecter manuellement un dictionnaire "globals", la valeur du
        # dictionnaire ainsi affecté NE CONTIENT PAS les méthodes qui n'ont
        # pas encore été créées. À titre d'exemple, considérez deux méthodes
        # personnalisées, toto() et zozo(); si toto() est créée en premier,
        # zozo() ne fera pas partie des éléments globaux auxquels toto() peut
        # accéder. Ceci constitue un problème car il arrive que les méthodes ou
        # fonctions personnalisées fassent référence à d'autres méthodes ou
        # fonctions personnalisées. Afin de résoudre ce problème, nous gardons
        # une trace des nouvelles fonctions que nous avons créées (grâce à
        # la variable new_functions) et nous ajoutons manuellement chaque
        # nouvelle fonction à la liste d'éléments globaux de chaque fonction
        # précédemment créée. Ainsi, à la fin de la boucle, chaque fonction
        # personnalisée sera à même de faire référence à toutes les autres.
        _reassign_globals(custom_methods, new_mod, new_class)
        _reassign_globals(custom_functions, new_mod, new_mod)
        _reassign_globals(manipulator_methods, new_mod, new_mod.__dict__['AddManipulator'])
        _reassign_globals(manipulator_methods, new_mod, new_mod.__dict__['ChangeManipulator'])

        if hasattr(new_class, 'get_absolute_url'):
            new_class.get_absolute_url = curry(get_absolute_url, opts, new_class.get_absolute_url)

        # On obtient une référence au module qui contient la classe et on lui ajoute
        # dynamiquement le nouveau module.
        app_package = sys.modules.get(new_class.__module__)
        if replaces_module is not None:
            app_label = replaces_module[0]
        else:
            app_package.__dict__[opts.module_name] = new_mod
            app_label = app_package.__name__[app_package.__name__.rfind('.')+1:]

            # On remplit le membre _MODELS du module qui contient la classe.
            # Exemple : django.models.sondages possèdera un membre _MODELS qui
            # contiendra cette liste :
            # [<class 'django.models.sondages.Sondage'>, <class 'django.models.sondages.Reponse'>]
            # On ne fait pas ça si replaces_module est actif.
            app_package.__dict__.setdefault('_MODELS', []).append(new_class)

        # On met en cache le label de l'application.
        opts.app_label = app_label

        # Si l'option db_table n'a pas été spécifiée, on utilise app_label + module_name.
        if not opts.db_table:
            opts.db_table = "%s_%s" % (app_label, opts.module_name)
        new_class._meta = opts

        # On affecte à l'attribut __file__ du nouveau module la valeur de
        # l'attribut __file__ de son paquet, car techniquement ils proviennent
        # du même fichier. Note : si on ne se donnait pas la peine de définir
        # cet attribut, sys.modules considèrerait le nouveau module comme un
        # module intégré.
        try:
            new_mod.__file__ = app_package.__file__
        except AttributeError:
            # L'objet 'module' ne possède pas d'attribut '__file__', ce qui veut dire
            # que la classe a probablement été entrée au moyen de l'interpréteur
            # interactif.
            pass

        # On ajoute une entrée correspondant au nouveau module dans sys.modules (à titre
        # d'exemple, "django.models.sondages.sondages"). Notez que l'entrée "django.models.sondages"
        # a déjà été ajoutée automatiquement.
        sys.modules.setdefault('%s.%s.%s' % (MODEL_PREFIX, app_label, opts.module_name), new_mod)

        # Si ce module en remplace un autre, on obtient une référence au parent
        # de l'autre module et on remplace l'autre module par celui que nous venons
        # de créer.
        if replaces_module is not None:
            old_app = get_app(replaces_module[0])
            setattr(old_app, replaces_module[1], new_mod)
            for i, model in enumerate(old_app._MODELS):
                if model._meta.module_name == replaces_module[1]:
                    # On remplace le membre approprié de la structure de données
                    # _MODELS de l'ancienne application.
                    old_app._MODELS[i] = new_class
                    # On remplace toutes les relations qui se faisaient avec
                    # l'ancienne classe par des relations se faisant avec la
                    # nouvelle classe.
                    for rel_opts, rel_field in model._meta.get_all_related_objects():
                        rel_field.rel.to = opts
                    for rel_opts, rel_field in model._meta.get_all_related_many_to_many_objects():
                        rel_field.rel.to = opts
                    break

        return new_class

class Model:
    __metaclass__ = ModelBase

    def __repr__(self):
        return '<Objet "%s">' % self.__class__.__name__

######################################################################
# FONCTIONS ASSISTANTES (LES MÉTHODES QUI SERONT ENSUITE CURRYFIÉES) #
######################################################################

# MÉTHODES DE BASE #########################

def method_init(opts, self, *args, **kwargs):
    for i, arg in enumerate(args):
        setattr(self, opts.fields[i].name, arg)
    for k, v in kwargs.items():
        try:
            opts.get_field(k, many_to_many=False)
        except FieldDoesNotExist:
            raise TypeError, "L'argument passé par mot-clé '%s' est invalide." % k
        setattr(self, k, v)

def method_eq(opts, self, other):
    return isinstance(other, self.__class__) and getattr(self, opts.pk.name) == getattr(other, opts.pk.name)

def method_save(opts, self):
    # On lance (le cas échéant) les hooks pré-enregistrement.
    if hasattr(self, '_pre_save'):
        self._pre_save()
    non_pks = [f for f in opts.fields if not f.primary_key]
    cursor = db.db.cursor()
    add = not bool(getattr(self, opts.pk.name))
    for f in non_pks:
        f.pre_save(self, getattr(self, f.name), add)
    db_values = [f.get_db_prep_save(getattr(self, f.name), add) for f in non_pks]
    # Les objets OneToOne constituent des cas particuliers car ils n'ont pas de
    # champ AutoField; on doit donc définir à la main le champ faisant office de
    # clé primaire.
    if isinstance(opts.pk.rel, OneToOne):
        cursor.execute("UPDATE %s SET %s WHERE %s=%%s" % \
            (opts.db_table, ','.join(['%s=%%s' % f.name for f in non_pks]),
            opts.pk.name), db_values + [getattr(self, opts.pk.name)])
        if cursor.rowcount == 0: # Si rien n'a été mis à jour, on ajoute l'enregistrement.
            field_names = [f.name for f in opts.fields]
            placeholders = ['%s'] * len(field_names)
            cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % \
                (opts.db_table, ','.join(field_names), ','.join(placeholders)),
                [f.get_db_prep_save(getattr(self, f.name), add=True) for f in opts.fields])
    else:
        if not add:
            cursor.execute("UPDATE %s SET %s WHERE %s=%%s" % \
                (opts.db_table, ','.join(['%s=%%s' % f.name for f in non_pks]),
                opts.pk.name), db_values + [getattr(self, opts.pk.name)])
        else:
            field_names = [f.name for f in non_pks]
            placeholders = ['%s'] * len(field_names)
            if opts.order_with_respect_to:
                field_names.append('_order')
                placeholders.append('(SELECT COUNT(*) FROM %s WHERE %s = %%s)' % \
                    (opts.db_table, opts.order_with_respect_to.name))
                db_values.append(getattr(self, opts.order_with_respect_to.name))
            cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % \
                (opts.db_table, ','.join(field_names), ','.join(placeholders)), db_values)
            setattr(self, opts.pk.name, db.get_last_insert_id(cursor, opts.db_table, opts.pk.name))
    db.db.commit()
    # On lance (le cas échéant) les hooks post-enregistrement.
    if hasattr(self, '_post_save'):
        self._post_save()

def method_delete(opts, self):
    assert getattr(self, opts.pk.name) is not None, "Impossible de supprimer l'objet %r car il ne possède pas d'identifiant."
    cursor = db.db.cursor()
    for rel_opts, rel_field in opts.get_all_related_objects():
        rel_opts_name = opts.get_rel_object_method_name(rel_opts, rel_field)
        if isinstance(rel_field.rel, OneToOne):
            try:
                sub_obj = getattr(self, 'get_%s' % rel_opts_name)()
            except ObjectDoesNotExist:
                pass
            else:
                sub_obj.delete()
        else:
            for sub_obj in getattr(self, 'get_%s_list' % rel_opts_name)():
                sub_obj.delete()
    for rel_opts, rel_field in opts.get_all_related_many_to_many_objects():
        cursor.execute("DELETE FROM %s WHERE %s_id=%%s" % (rel_field.get_m2m_db_table(rel_opts),
            self._meta.object_name.lower()), [getattr(self, opts.pk.name)])
    cursor.execute("DELETE FROM %s WHERE %s=%%s" % (opts.db_table, opts.pk.name), [getattr(self, opts.pk.name)])
    db.db.commit()
    setattr(self, opts.pk.name, None)
    for f in opts.fields:
        if isinstance(f, FileField) and getattr(self, f.name):
            file_name = getattr(self, 'get_%s_filename' % f.name)()
            # Si le fichier concerné existe, et si aucun autre objet du type FileField n'y fait
            # référence, on le retire du système de fichiers.
            if os.path.exists(file_name) and not opts.get_model_module().get_list(**{'%s__exact' % f.name: getattr(self, f.name)}):
                os.remove(file_name)

def method_get_next_in_order(opts, order_field, self):
    if not hasattr(self, '_next_in_order_cache'):
        self._next_in_order_cache = opts.get_model_module().get_object(order_by=(('_order', 'ASC'),),
            where=['_order > (SELECT _order FROM %s WHERE %s=%%s)' % (opts.db_table, opts.pk.name),
                '%s=%%s' % order_field.name], limit=1,
            params=[getattr(self, opts.pk.name), getattr(self, order_field.name)])
    return self._next_in_order_cache

def method_get_previous_in_order(opts, order_field, self):
    if not hasattr(self, '_previous_in_order_cache'):
        self._previous_in_order_cache = opts.get_model_module().get_object(order_by=(('_order', 'DESC'),),
            where=['_order < (SELECT _order FROM %s WHERE %s=%%s)' % (opts.db_table, opts.pk.name),
                '%s=%%s' % order_field.name], limit=1,
            params=[getattr(self, opts.pk.name), getattr(self, order_field.name)])
    return self._previous_in_order_cache

# MÉTHODES LIÉES AUX RELATIONS #############

# Exemple : Article.get_ligne_date_lieu()
def method_get_many_to_one(field_with_rel, self):
    cache_var = field_with_rel.rel.get_cache_name()
    if not hasattr(self, cache_var):
        val = getattr(self, field_with_rel.name)
        mod = field_with_rel.rel.to.get_model_module()
        if val is None:
            raise getattr(mod, '%sDoesNotExist' % field_with_rel.rel.to.object_name)
        retrieved_obj = mod.get_object(**{'%s__exact' % field_with_rel.rel.field_name: val})
        setattr(self, cache_var, retrieved_obj)
    return getattr(self, cache_var)

# Gère l'obtention des objets liés par une relation plusieurs-plusieurs.
# Exemple : Sondage.get_site_list()
def method_get_many_to_many(field_with_rel, self):
    rel = field_with_rel.rel.to
    cache_var = '_%s_cache' % field_with_rel.name
    if not hasattr(self, cache_var):
        mod = rel.get_model_module()
        sql = "SELECT %s FROM %s a, %s b WHERE a.%s = b.%s_id AND b.%s_id = %%s %s" % \
            (','.join(['a.%s' % f.name for f in rel.fields]), rel.db_table,
            field_with_rel.get_m2m_db_table(self._meta), rel.pk.name,
            rel.object_name.lower(), self._meta.object_name.lower(), rel.get_order_sql('a'))
        cursor = db.db.cursor()
        cursor.execute(sql, [getattr(self, self._meta.pk.name)])
        setattr(self, cache_var, [getattr(mod, rel.object_name)(*row) for row in cursor.fetchall()])
    return getattr(self, cache_var)

# Gère l'affectation d'une relation plusieurs-plusieurs.
# Exemple : Sondage.set_sites()
def method_set_many_to_many(rel_field, self, id_list):
    id_list = map(int, id_list) # on normalise les identifiants afin de travailler avec une liste d'entiers
    current_ids = [obj.id for obj in method_get_many_to_many(rel_field, self)]
    ids_to_add, ids_to_delete = dict([(i, 1) for i in id_list]), []
    for current_id in current_ids:
        if current_id in id_list:
            del ids_to_add[current_id]
        else:
            ids_to_delete.append(current_id)
    ids_to_add = ids_to_add.keys()
    # ids_to_add constitue maintenant une liste des identifiants à ajouter, ids_to_delete une liste des identifiants à supprimer
    if not ids_to_delete and not ids_to_add:
        return False # pas de changement
    rel = rel_field.rel.to
    m2m_table = rel_field.get_m2m_db_table(self._meta)
    cursor = db.db.cursor()
    this_id = getattr(self, self._meta.pk.name)
    if ids_to_delete:
        sql = "DELETE FROM %s WHERE %s_id = %%s AND %s_id IN (%s)" % (m2m_table, self._meta.object_name.lower(), rel.object_name.lower(), ','.join(map(str, ids_to_delete)))
        cursor.execute(sql, [this_id])
    if ids_to_add:
        sql = "INSERT INTO %s (%s_id, %s_id) VALUES (%%s, %%s)" % (m2m_table, self._meta.object_name.lower(), rel.object_name.lower())
        cursor.executemany(sql, [(this_id, i) for i in ids_to_add])
    db.db.commit()
    try:
        delattr(self, '_%s_cache' % rel_field.name) # on efface le cache, s'il existe
    except AttributeError:
        pass
    return True

# Gère l'obtention d'objets connexes.
# Exemples : Sondage.get_reponse(), Sondage.get_reponse_list(), Sondage.get_reponse_count()
def method_get_related(method_name, rel_mod, rel_field, self, **kwargs):
    kwargs['%s__exact' % rel_field.name] = getattr(self, rel_field.rel.field_name)
    kwargs.update(rel_field.rel.lookup_overrides)
    return getattr(rel_mod, method_name)(**kwargs)

# Gère l'ajout d'objets connexes.
# Exemple : Sondage.add_reponse()
def method_add_related(rel_obj, rel_mod, rel_field, self, *args, **kwargs):
    init_kwargs = dict(zip([f.name for f in rel_obj.fields if f != rel_field and not isinstance(f, AutoField)], args))
    init_kwargs.update(kwargs)
    for f in rel_obj.fields:
        if isinstance(f, AutoField):
            init_kwargs[f.name] = None
    init_kwargs[rel_field.name] = getattr(self, rel_field.rel.field_name)
    obj = rel_mod.Klass(**init_kwargs)
    obj.save()
    return obj

# Gère l'obtention des objets connexes à l'autre bout d'une relation plusieurs-plusieurs.
# Exemples : Album.get_chanson(), Album.get_chanson_list(), Album.get_chanson_count()
def method_get_related_many_to_many(method_name, rel_mod, rel_field, self, **kwargs):
    kwargs['%s__id__exact' % rel_field.name] = self.id
    return getattr(rel_mod, method_name)(**kwargs)

# Gère la définition d'objets liés par une relation plusieurs-plusieurs.
# Exemple : Album.set_chansons()
def method_set_related_many_to_many(rel_opts, rel_field, self, id_list):
    id_list = map(int, id_list) # on normalise les identifiants afin de travailler avec une liste d'entiers
    rel = rel_field.rel.to
    m2m_table = rel_field.get_m2m_db_table(rel_opts)
    this_id = getattr(self, self._meta.pk.name)
    cursor = db.db.cursor()
    cursor.execute("DELETE FROM %s WHERE %s_id = %%s" % (m2m_table, rel.object_name.lower()), [this_id])
    sql = "INSERT INTO %s (%s_id, %s_id) VALUES (%%s, %%s)" % (m2m_table, rel.object_name.lower(), rel_opts.object_name.lower())
    cursor.executemany(sql, [(this_id, i) for i in id_list])
    db.db.commit()

# MÉTHODES DE TRI ##########################

def method_set_order(ordered_obj, self, id_list):
    cursor = db.db.cursor()
    # Exemple : "UPDATE sondage_reponses SET _order = %s WHERE sondage_id = %s AND id = %s"
    sql = "UPDATE %s SET _order = %%s WHERE %s = %%s AND %s = %%s" % (ordered_obj.db_table, ordered_obj.order_with_respect_to.name, ordered_obj.pk.name)
    rel_val = getattr(self, ordered_obj.order_with_respect_to.rel.field_name)
    cursor.executemany(sql, [(i, rel_val, j) for i, j in enumerate(id_list)])
    db.db.commit()

def method_get_order(ordered_obj, self):
    cursor = db.db.cursor()
    # Exemple : "SELECT id FROM sondage_reponses WHERE sondage_id = %s ORDER BY _order"
    sql = "SELECT %s FROM %s WHERE %s = %%s ORDER BY _order" % (ordered_obj.pk.name, ordered_obj.db_table, ordered_obj.order_with_respect_to.name)
    rel_val = getattr(self, ordered_obj.order_with_respect_to.rel.field_name)
    cursor.execute(sql, [rel_val])
    return [r[0] for r in cursor.fetchall()]

# MÉTHODES LIÉES AUX DATES #################

def method_get_next_or_previous(get_object_func, field, is_next, self, **kwargs):
    kwargs.setdefault('where', []).append('%s %s %%s' % (field.name, (is_next and '>' or '<')))
    kwargs.setdefault('params', []).append(str(getattr(self, field.name)))
    kwargs['order_by'] = ((field.name, (is_next and 'ASC' or 'DESC')),)
    kwargs['limit'] = 1
    return get_object_func(**kwargs)

# MÉTHODES LIÉES AUX FICHIERS ##############

def method_get_file_filename(field, self):
    return os.path.join(settings.MEDIA_ROOT, getattr(self, field.name))

def method_get_file_url(field, self):
    if getattr(self, field.name): # la valeur du champ n'est pas vierge
        import urlparse
        return urlparse.urljoin(settings.MEDIA_URL, getattr(self, field.name))
    return ''

def method_get_file_size(field, self):
    return os.path.getsize(method_get_file_filename(field, self))

def method_save_file(field, self, filename, raw_contents):
    directory = field.get_directory_name()
    try: # On crée le répertoire (dont le nom dépend de la date actuelle) s'il n'existe pas déjà.
        os.makedirs(os.path.join(settings.MEDIA_ROOT, directory))
    except OSError: # Le répertoire existe probablement déjà.
        pass
    filename = field.get_filename(filename)

    # Si un fichier du même nom existe déjà, on ajoute un tiret bas à la fin du nom
    # jusqu'à obtention d'un nom de fichier qui n'existe pas déjà.
    while os.path.exists(os.path.join(settings.MEDIA_ROOT, filename)):
        try:
            dot_index = filename.rindex('.')
        except ValueError: # l'argument filename ne contient pas de point
            filename += '_'
        else:
            filename = filename[:dot_index] + '_' + filename[dot_index:]

    # On écrit le fichier sur le disque.
    setattr(self, field.name, filename)
    fp = open(getattr(self, 'get_%s_filename' % field.name)(), 'w')
    fp.write(raw_contents)
    fp.close()

    # On enregistre la longueur et/ou hauteur de l'image, le cas échéant.
    if isinstance(field, ImageField) and (field.width_field or field.height_field):
        from django.utils.images import get_image_dimensions
        width, height = get_image_dimensions(getattr(self, 'get_%s_filename' % field.name)())
        if field.width_field:
            setattr(self, field.width_field, width)
        if field.height_field:
            setattr(self, field.height_field, height)

    # On enregistre l'objet car il a été modifié.
    self.save()

# MÉTHODES DES CHAMPS ImageField ###########

def method_get_image_width(field, self):
    return _get_image_dimensions(field, self)[0]

def method_get_image_height(field, self):
    return _get_image_dimensions(field, self)[1]

def _get_image_dimensions(field, self):
    cachename = "__%s_dimensions_cache" % field.name
    if not hasattr(self, cachename):
        from django.utils.images import get_image_dimensions
        fname = getattr(self, "get_%s_filename" % field.name)()
        setattr(self, cachename, get_image_dimensions(fname))
    return getattr(self, cachename)

#######################################################################
# FONCTIONS ASSISTANTES (LES FONCTIONS QUI SERONT ENSUITE CURRYFIÉES) #
#######################################################################

def get_absolute_url(opts, func, self):
    return settings.ABSOLUTE_URL_OVERRIDES.get('%s.%s' % (opts.app_label, opts.module_name), func)(self)

def _get_where_clause(lookup_type, table_prefix, field_name, value):
    try:
        return '%s%s %s %%s' % (table_prefix, field_name, db.OPERATOR_MAPPING[lookup_type])
    except KeyError:
        pass
    if lookup_type in ('range', 'year'):
        return '%s%s BETWEEN %%s AND %%s' % (table_prefix, field_name)
    elif lookup_type in ('month', 'day'):
        return "%s = %%s" % db.get_date_extract_sql(lookup_type, table_prefix + field_name)
    elif lookup_type == 'isnull':
        return "%s%s IS %sNULL" % (table_prefix, field_name, (not value and 'NOT ' or ''))
    raise TypeError, "Argument lookup_type invalide : %s" % repr(lookup_type)

def function_get_object(opts, klass, does_not_exist_exception, **kwargs):
    obj_list = function_get_list(opts, klass, **kwargs)
    if len(obj_list) < 1:
        raise does_not_exist_exception, "Aucun objet %s ne correspond aux paramètres de recherche %s" % (opts.object_name, kwargs)
    assert len(obj_list) == 1, "get_object() a renvoyé plus d'un objet %s : get_object a renvoyé %s objets! Les paramètres spécifiés pour la recherche étaient : %s" % (opts.object_name, len(obj_list), kwargs)
    return obj_list[0]

def _get_cached_row(opts, row, index_start):
    "Fonction assistante qui remplit le cache d'un objet et renvoie ensuite l'objet; le traitement est récursif"
    index_end = index_start + len(opts.fields)
    obj = opts.get_model_module().Klass(*row[index_start:index_end])
    for f in opts.fields:
        if f.rel and not f.null:
            rel_obj, index_end = _get_cached_row(f.rel.to, row, index_end)
            setattr(obj, f.rel.get_cache_name(), rel_obj)
    return obj, index_end

def function_get_list(opts, klass, **kwargs):
    # kwargs['select'] est un dictionnaire et comme l'ordre des clés d'un
    # dictionnaire n'est pas défini, on convertit en interne kwargs['select']
    # en une liste de tuples
    kwargs['select'] = kwargs.get('select', {}).items()

    cursor = db.db.cursor()
    select, sql, params = function_get_sql_clause(opts, **kwargs)
    cursor.execute("SELECT " + (kwargs.get('distinct') and "DISTINCT " or "") + ",".join(select) + sql, params)
    obj_list = []
    fill_cache = kwargs.get('select_related')
    index_end = len(opts.fields)
    for row in cursor.fetchall():
        if fill_cache:
            obj, index_end = _get_cached_row(opts, row, 0)
        else:
            obj = klass(*row[:index_end])
        for i, k in enumerate(kwargs['select']):
            setattr(obj, k[0], row[index_end+i])
        obj_list.append(obj)
    return obj_list

def function_get_iterator(opts, klass, **kwargs):
    # kwargs['select'] est un dictionnaire et comme l'ordre des clés d'un
    # dictionnaire n'est pas défini, on convertit en interne kwargs['select']
    # en une liste de tuples
    kwargs['select'] = kwargs.get('select', {}).items()

    cursor = db.db.cursor()
    select, sql, params = function_get_sql_clause(opts, **kwargs)
    cursor.execute("SELECT " + (kwargs.get('distinct') and "DISTINCT " or "") + ",".join(select) + sql, params)
    fill_cache = kwargs.get('select_related')
    index_end = len(opts.fields)
    while 1:
        rows = cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE)
        if not rows:
            raise StopIteration
        for row in rows:
            if fill_cache:
                obj, index_end = _get_cached_row(opts, row, 0)
            else:
                obj = klass(*row[:index_end])
            for i, k in enumerate(kwargs['select']):
                setattr(obj, k[0], row[index_end+i])
            yield obj

def function_get_count(opts, **kwargs):
    kwargs['order_by'] = []
    kwargs['offset'] = None
    kwargs['limit'] = None
    kwargs['select_related'] = False
    _, sql, params = function_get_sql_clause(opts, **kwargs)
    cursor = db.db.cursor()
    cursor.execute("SELECT COUNT(*)" + sql, params)
    return cursor.fetchone()[0]

def _fill_table_cache(opts, select, tables, where, old_prefix, cache_tables_seen):
    """
    Fonction assistante qui se charge de remplir récursivement les variables select,
    tables et where (qui sont modifiées directement) pour le compte des requêtes
    fill-cache.
    """
    for f in opts.fields:
        if f.rel and not f.null:
            db_table = f.rel.to.db_table
            if db_table not in cache_tables_seen:
                tables.append(db_table)
            else: # On a déjà rencontré cette table, on lui donne donc un alias.
                new_prefix = '%s%s' % (db_table, len(cache_tables_seen))
                tables.append('%s %s' % (db_table, new_prefix))
                db_table = new_prefix
            cache_tables_seen.append(db_table)
            where.append('%s.%s = %s.%s' % (old_prefix, f.name, db_table, f.rel.field_name))
            select.extend(['%s.%s' % (db_table, f2.name) for f2 in f.rel.to.fields])
            _fill_table_cache(f.rel.to, select, tables, where, db_table, cache_tables_seen)

def _throw_bad_kwarg_error(kwarg):
    # Fonction assistante qui nous évite d'être redondant.
    raise TypeError, "l'argument passé par mot-clé '%s' est inconnu" % kwarg

def _parse_lookup(kwarg_items, opts, table_count=0):
    # Fonction assistante qui gère la conversion des arguments passés par mot-clé qu'utilise
    # l'API ("nom__exact": "tom", par exemple) en code SQL.

    # Notez qu'il y a une différence entre les variables where et join_where. join_where
    # est une liste des clauses WHERE qui serviront pour les requêtes JOIN. L'existence
    # d'une telle différenciation est rendue nécessaire par la prise en charge de l'argument
    # "_or".

    # L'argument table_count nous permet de nous assurer que les alias utilisés pour les
    # tables sont bien uniques.
    tables, join_where, where, params = [], [], [], []
    for kwarg, kwarg_value in kwarg_items:
        if kwarg in ('order_by', 'limit', 'offset', 'select_related', 'distinct', 'select', 'tables', 'where', 'params'):
            continue
        if kwarg_value is None:
            continue
        if kwarg == '_or':
            for val in kwarg_value:
                tables2, join_where2, where2, params2, table_count = _parse_lookup(val, opts, table_count)
                tables.extend(tables2)
                join_where.extend(join_where2)
                where.append('(%s)' % ' OR '.join(where2))
                params.extend(params2)
            continue
        lookup_list = kwarg.split(LOOKUP_SEPARATOR)
        if len(lookup_list) == 1:
            _throw_bad_kwarg_error(kwarg)
        lookup_type = lookup_list.pop()
        current_opts = opts # On va modifier current_opts, il est donc bon de conserver une référence à la variable opts d'origine dans le code appelant.
        current_table_alias = current_opts.db_table
        param_required = False
        while lookup_list or param_required:
            table_count += 1
            try:
                # La variable current est un élément de la liste lookup_list. À titre d'exemple,
                # avec l'instruction reponses.get_list(sondage__sites__id__exact=5), la variable
                # lookup_list sera ["sondages", "sites", "id"] et la première valeur que prendra
                # la variable current sera "sondages".
                try:
                    current = lookup_list.pop(0)
                except IndexError:
                    # Lorsqu'on est ici, la variable lookup_list est vide et la
                    # variable param_required vaut True, ce qui signifie que
                    # l'argument passé par mot-clé est incorrect.
                    # Exemple : reponses.get_list(sondage__exact='toto')
                    _throw_bad_kwarg_error(kwarg)
                # On regarde d'abord les relations plusieurs-plusieurs...
                for f in current_opts.many_to_many:
                    if f.name == current:
                        rel_table_alias = 't%s' % table_count
                        table_count += 1
                        tables.append('%s %s' % (f.get_m2m_db_table(current_opts), rel_table_alias))
                        join_where.append('%s.%s = %s.%s_id' % (current_table_alias, current_opts.pk.name,
                            rel_table_alias, current_opts.object_name.lower()))
                        # Optimisation : si la recherche concerne une clé primaire, nul
                        # besoin de faire une jointure supplémentaire.
                        if lookup_list and lookup_list[0] == f.rel.to.pk.name and lookup_type == 'exact':
                            where.append(_get_where_clause(lookup_type, rel_table_alias+'.',
                                f.rel.to.object_name.lower()+'_id', kwarg_value))
                            params.extend(f.get_db_prep_lookup(lookup_type, kwarg_value))
                            lookup_list.pop()
                            param_required = False
                        else:
                            new_table_alias = 't%s' % table_count
                            tables.append('%s %s' % (f.rel.to.db_table, new_table_alias))
                            join_where.append('%s.%s_id = %s.%s' % (rel_table_alias, f.rel.to.object_name.lower(),
                                new_table_alias, f.rel.to.pk.name))
                            current_table_alias = new_table_alias
                            param_required = True
                        current_opts = f.rel.to
                        raise StopIteration
                for f in current_opts.fields:
                    # On regarde les relations plusieurs-un...
                    if f.rel and f.rel.name == current:
                        # Optimisation : si la recherche concerne une clé primaire, nul
                        # besoin de faire une jointure supplémentaire.
                        if lookup_list and lookup_list[0] == f.rel.to.pk.name and lookup_type == 'exact':
                            where.append(_get_where_clause(lookup_type, current_table_alias+'.', f.name, kwarg_value))
                            params.extend(f.get_db_prep_lookup(lookup_type, kwarg_value))
                            lookup_list.pop()
                            param_required = False
                        else:
                            new_table_alias = 't%s' % table_count
                            tables.append('%s %s' % (f.rel.to.db_table, new_table_alias))
                            join_where.append('%s.%s = %s.%s' % (current_table_alias, f.name, new_table_alias, f.rel.to.pk.name))
                            current_table_alias = new_table_alias
                            param_required = True
                        current_opts = f.rel.to
                        raise StopIteration
                    # On regarde directement les noms de champ...
                    if f.name == current:
                        where.append(_get_where_clause(lookup_type, current_table_alias+'.', current, kwarg_value))
                        params.extend(f.get_db_prep_lookup(lookup_type, kwarg_value))
                        param_required = False
                        raise StopIteration
                # Si StopIteration n'a pas été déclenchée, c'est que la variable current
                # fait référence à un nom invalide et on déclenche donc une exception.
                _throw_bad_kwarg_error(kwarg)
            except StopIteration:
                continue
    return tables, join_where, where, params, table_count

def function_get_sql_clause(opts, **kwargs):
    select = ["%s.%s" % (opts.db_table, f.name) for f in opts.fields]
    tables = [opts.db_table] + (kwargs.get('tables') and kwargs['tables'][:] or [])
    where = kwargs.get('where') and kwargs['where'][:] or []
    params = kwargs.get('params') and kwargs['params'][:] or []

    # On convertit les arguments passés par mot-clé en code SQL.
    tables2, join_where2, where2, params2, _ = _parse_lookup(kwargs.items(), opts)
    tables.extend(tables2)
    where.extend(join_where2 + where2)
    params.extend(params2)

    # On ajoute les contraintes supplémentaires qui proviennent du paramètre "where_constraints".
    where.extend(opts.where_constraints)

    # Si l'option select_related est activée, on ajoute les tables et clauses WHERE supplémentaires.
    if kwargs.get('select_related') is True:
        _fill_table_cache(opts, select, tables, where, opts.db_table, [opts.db_table])

    # On ajoute toute requête SELECT supplémentaire qui aurait été transmise par mot-clé.
    if kwargs.get('select', False):
        select.extend(['(%s) AS %s' % (s[1], s[0]) for s in kwargs['select']])

    # On s'occupe de la clause ORDER BY
    order_by = []
    for i, j in kwargs.get('order_by', opts.ordering):
        if j == "RANDOM":
            order_by.append("RANDOM()")
        else:
            # On met avant le nom de la colonne le nom de la table de base de données
            # si cela n'a pas déjà été fait, et aussi si la colonne en question ne fait
            # pas référence à une requête SELECT personnalisée.
            if "." not in i and i not in [k[0] for k in kwargs.get('select', [])]:
                order_by.append("%s.%s %s" % (opts.db_table, i, j))
            else:
                order_by.append("%s %s" % (i, j))
    order_by = ", ".join(order_by)

    # On s'occupe des clauses LIMIT et OFFSET
    if kwargs.get('limit') is not None:
        limit_sql = " LIMIT %s " % kwargs['limit']
        if kwargs.get('offset') is not None and kwargs['offset'] != 0:
            limit_sql += "OFFSET %s " % kwargs['offset']
    else:
        limit_sql = ""

    return select, " FROM " + ",".join(tables) + (where and " WHERE " + " AND ".join(where) or "") + (order_by and " ORDER BY " + order_by or "") + limit_sql, params

def function_get_in_bulk(opts, klass, *args, **kwargs):
    id_list = args and args[0] or kwargs['id_list']
    assert id_list != [], "Vous ne pouvez pas passer une liste vide à get_in_bulk()."
    kwargs['where'] = ["%s.id IN (%s)" % (opts.db_table, ",".join(map(str, id_list)))]
    obj_list = function_get_list(opts, klass, **kwargs)
    return dict([(o.id, o) for o in obj_list])

def function_get_latest(opts, klass, does_not_exist_exception, **kwargs):
    kwargs['order_by'] = ((opts.get_latest_by, "DESC"),)
    kwargs['limit'] = 1
    return function_get_object(opts, klass, does_not_exist_exception, **kwargs)

def function_get_date_list(opts, field, *args, **kwargs):
    from django.core.db.typecasts import typecast_timestamp
    kind = args and args[0] or kwargs['kind']
    assert kind in ("month", "year", "day"), "L'argument kind peut prendre pour valeur 'year', 'month' ou 'day'."
    order = 'ASC'
    if kwargs.has_key('_order'):
        order = kwargs['_order']
        del kwargs['_order']
    assert order in ('ASC', 'DESC'), "L'argument order peut prendre pour valeur 'ASC' ou 'DESC'"
    kwargs['order_by'] = [] # On efface cette variable car sinon cela risque de tout compliquer.
    if field.null:
        kwargs.setdefault('where', []).append('%s.%s IS NOT NULL' % (opts.db_table, field.name))
    select, sql, params = function_get_sql_clause(opts, **kwargs)
    sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1' % (db.get_date_trunc_sql(kind, '%s.%s' % (opts.db_table, field.name)), sql)
    cursor = db.db.cursor()
    cursor.execute(sql, params)
    # Il faut que nous appelions manuellement typecast_timestamp(str()) sur le
    # résultat, car MySQL ne convertit pas automatiquement le résultat des fonctions
    # liées aux dates en objets datetime : MySQL renvoie plutôt les valeurs sous
    # forme de chaînes.
    return [typecast_timestamp(str(row[0])) for row in cursor.fetchall()]

#########################################
# FONCTIONS ASSISTANTES (MANIPULATEURS) #
#########################################

def get_manipulator(opts, klass, extra_methods, add=False, change=False):
    "Renvoie un manipulateur personnalisé (soit pour l'ajout, soit pour la modification) correspondant à l'argument opts spécifié."
    assert (add == False or change == False) and add != change, "Vous pouvez passer à la fonction get_manipulator() add=True ou change=True mais pas les deux en même temps"
    man = types.ClassType('%sManipulator%s' % (opts.object_name, add and 'Add' or 'Change'), (formfields.Manipulator,), {})
    man.__module__ = MODEL_PREFIX + '.' + opts.module_name # On définit __module__ explicitement, comme ci-dessus.
    man.__init__ = curry(manipulator_init, opts, add, change)
    man.save = curry(manipulator_save, opts, klass, add, change)
    for field_name_list in opts.unique_together:
        setattr(man, 'isUnique%s' % '_'.join(field_name_list), curry(manipulator_validator_unique_together, field_name_list, opts))
    for f in opts.fields:
        if f.unique_for_date:
            setattr(man, 'isUnique%sFor%s' % (f.name, f.unique_for_date), curry(manipulator_validator_unique_for_date, f, opts.get_field(f.unique_for_date), opts, 'date'))
        if f.unique_for_month:
            setattr(man, 'isUnique%sFor%s' % (f.name, f.unique_for_month), curry(manipulator_validator_unique_for_date, f, opts.get_field(f.unique_for_month), opts, 'month'))
        if f.unique_for_year:
            setattr(man, 'isUnique%sFor%s' % (f.name, f.unique_for_year), curry(manipulator_validator_unique_for_date, f, opts.get_field(f.unique_for_year), opts, 'year'))
    for k, v in extra_methods.items():
        setattr(man, k, v)
    return man

def manipulator_init(opts, add, change, self, obj_key=None):
    if change:
        assert obj_key is not None, "Vous devez passer à ChangeManipulator.__init__() un paramètre obj_key."
        self.obj_key = obj_key
        try:
            self.original_object = opts.get_model_module().get_object(**{'%s__exact' % opts.pk.name: obj_key})
        except ObjectDoesNotExist:
            # Si l'objet n'existe pas, c'est peut-être que l'on veut créer un manipulateur
            # associé à un objet situé à l'autre bout d'une relation un-un, et que cet objet
            # n'a pas encore été créé. Par exemple, on veut peut-être créer un manipulateur
            # pour un Restaurant lié à un Lieu qui n'a pas encore de Restaurant.
            if opts.one_to_one_field:
                # On vérifie par mesure de précaution si un objet parent existe.
                # Pour reprendre l'exemple plus haut, on s'assure que le Lieu
                # correspondant au nouveau Restaurant existe bien; si ce n'est pas
                # le cas, on laisse l'exception ObjectDoesNotExist se propager.
                lookup_kwargs = opts.one_to_one_field.rel.limit_choices_to
                lookup_kwargs['%s__exact' % opts.one_to_one_field.rel.field_name] = obj_key
                _ = opts.one_to_one_field.rel.to.get_model_module().get_object(**lookup_kwargs)
                params = dict([(f.name, f.get_default()) for f in opts.fields])
                params[opts.pk.name] = obj_key
                self.original_object = opts.get_model_module().Klass(**params)
            else:
                raise
    self.fields = []
    for f in opts.fields + opts.many_to_many:
        if f.editable and (not f.rel or not f.rel.edit_inline):
            self.fields.extend(f.get_manipulator_fields(opts, self, change))

    # On ajoute les champs correspondant aux objets connexes.
    for rel_opts, rel_field in opts.get_inline_related_objects():
        if change:
            count = getattr(self.original_object, 'get_%s_count' % opts.get_rel_object_method_name(rel_opts, rel_field))()
            count += rel_field.rel.num_extra_on_change
            if rel_field.rel.min_num_in_admin:
                count = max(count, rel_field.rel.min_num_in_admin)
            if rel_field.rel.max_num_in_admin:
                count = min(count, rel_field.rel.max_num_in_admin)
        else:
            count = rel_field.rel.num_in_admin
        for f in rel_opts.fields + rel_opts.many_to_many:
            if f.editable and f != rel_field and (not f.primary_key or (f.primary_key and change)):
                for i in range(count):
                    self.fields.extend(f.get_manipulator_fields(rel_opts, self, change, name_prefix='%s.%d.' % (rel_opts.object_name.lower(), i), rel=True))

    # On ajoute un champ pour le tri.
    if change and opts.get_ordered_objects():
        self.fields.append(formfields.CommaSeparatedIntegerField(field_name="order_"))

def manipulator_save(opts, klass, add, change, self, new_data):
    from django.utils.datastructures import DotExpandedDict
    params = {}
    for f in opts.fields:
        # Les champs pour lesquels l'option auto_now_add est activée constituent
        # un cas particulier; ils doivent conserver leur valeur et ce même si
        # l'on se trouve en mode "changement".
        if change and getattr(f, 'auto_now_add', False):
            params[f.name] = getattr(self.original_object, f.name)
        else:
            params[f.name] = f.get_manipulator_new_data(new_data)

    if change:
        params[opts.pk.name] = self.obj_key

    # On enregistre d'abord l'objet de base.
    new_object = klass(**params)
    new_object.save()

    # Maintenant que l'objet a été enregistré, on enregistre tout fichier qui aurait été uploadé.
    for f in opts.fields:
        if isinstance(f, FileField):
            f.save_file(new_data, new_object, change and self.original_object or None, change, rel=False)

    # On regarde quels champs ont changé.
    if change:
        self.fields_added, self.fields_changed, self.fields_deleted = [], [], []
        for f in opts.fields:
            if not f.primary_key and str(getattr(self.original_object, f.name)) != str(getattr(new_object, f.name)):
                self.fields_changed.append(f.verbose_name)

    # On enregistre les objets liés à l'objet actuel par une relation plusieurs-plusieurs. Exemple : Sondage.set_sites()
    for f in opts.many_to_many:
        if not f.rel.edit_inline:
            was_changed = getattr(new_object, 'set_%s' % f.name)(new_data.getlist(f.name))
            if change and was_changed:
                self.fields_changed.append(f.verbose_name)

    # On enregistre les objets liés à l'objet actuel par une relation plusieurs-un. Exemple : on ajoute les objets Reponse d'un Sondage.
    for rel_opts, rel_field in opts.get_inline_related_objects():
        # On crée la variable obj_list, une instance de la classe DotExpandedDict du style :
        # [('0', {'id': ['940'], 'reponse': ['Ceci est la première réponse']}),
        #  ('1', {'id': ['941'], 'reponse': ['Ceci est la deuxième réponse']}),
        #  ('2', {'id': [''], 'reponse': ['']})]
        obj_list = DotExpandedDict(new_data.data)[rel_opts.object_name.lower()].items()
        obj_list.sort(lambda x, y: cmp(int(x[0]), int(y[0])))
        params = {}

        # Pour chaque objet connexe...
        for _, rel_new_data in obj_list:

            # Ces deux variables nous indiquent quels champs pour lesquels core=True
            # ont été spécifiés. Si tous les champs pour lesquels core=True ont été
            # spécifiés, l'objet correspondant sera enregistré. Si aucun des champs
            # pour lesquels core=True n'a été spécifié, l'objet correspondant sera
            # supprimé. Si certains mais pas tous les champs pour lesquels core=True
            # ont été spécifiés, le validateur l'aurait signalé.
            all_cores_given, all_cores_blank = True, True

            # On obtient une référence à l'ancien objet. Elle nous servira à comparer
            # l'ancien objet au nouveau et à voir les champs qui ont changé.
            if change:
                old_rel_obj = None
                if rel_new_data[rel_opts.pk.name][0]:
                    try:
                        old_rel_obj = getattr(self.original_object, 'get_%s' % opts.get_rel_object_method_name(rel_opts, rel_field))(**{'%s__exact' % rel_opts.pk.name: rel_new_data[rel_opts.pk.name][0]})
                    except ObjectDoesNotExist:
                        pass

            for f in rel_opts.fields:
                if f.core and not isinstance(f, FileField) and f.get_manipulator_new_data(rel_new_data, rel=True) in (None, ''):
                    all_cores_given = False
                elif f.core and not isinstance(f, FileField) and f.get_manipulator_new_data(rel_new_data, rel=True) not in (None, ''):
                    all_cores_blank = False
                # Si le champ ne peut pas être modifié, on lui donne la même valeur
                # que précédemment; on obtient cette valeur grâce à l'identifiant qui
                # nous a été spécifié via rel_opts. Si aucun identifiant ne nous a été
                # spécifié, on utilise une valeur par défaut. Les champs de type
                # FileField constituent un cas particulier et on se chargera d'eux plus
                # tard.
                if change and (isinstance(f, FileField) or not f.editable):
                    if rel_new_data.get(rel_opts.pk.name, False) and rel_new_data[rel_opts.pk.name][0]:
                        params[f.name] = getattr(old_rel_obj, f.name)
                    else:
                        params[f.name] = f.get_default()
                elif f == rel_field:
                    params[f.name] = getattr(new_object, rel_field.rel.field_name)
                elif add and isinstance(f, AutoField):
                    params[f.name] = None
                else:
                    params[f.name] = f.get_manipulator_new_data(rel_new_data, rel=True)
                # Les liens connexes constituent un cas particulier, car on doit
                # définir à la main le champ "content_type_id".
                if opts.has_related_links and rel_opts.module_name == 'relatedlinks':
                    contenttypes_mod = get_module('core', 'contenttypes')
                    params['content_type_id'] = contenttypes_mod.get_object(package__label__exact=opts.app_label, python_module_name__exact=opts.module_name).id
                    params['object_id'] = new_object.id

            # On crée l'objet connexe.
            new_rel_obj = rel_opts.get_model_module().Klass(**params)

            # Si tous les champs pour lesquels core=True ont été spécifiés (et s'ils sont non-vides), on enregistre l'objet.
            if all_cores_given:
                new_rel_obj.save()

                # On enregistre les fichiers uploadés.
                for f in rel_opts.fields:
                    if isinstance(f, FileField) and rel_new_data.get(f.name, False):
                        f.save_file(rel_new_data, new_rel_obj, change and old_rel_obj or None, change, rel=True)

                # On regarde quels champs ont changé.
                if change:
                    if not old_rel_obj: # L'objet actuel n'existait pas auparavant.
                        self.fields_added.append('%s "%r"' % (rel_opts.verbose_name, new_rel_obj))
                    else:
                        for f in rel_opts.fields:
                            if not f.primary_key and f != rel_field and str(getattr(old_rel_obj, f.name)) != str(getattr(new_rel_obj, f.name)):
                                self.fields_changed.append('%s de l\'objet %s "%r"' % (f.verbose_name, rel_opts.verbose_name, new_rel_obj))

                # On enregistre les objets liés à l'objet actuel par une relation plusieurs-plusieurs.
                for f in rel_opts.many_to_many:
                    if not f.rel.edit_inline:
                        was_changed = getattr(new_rel_obj, 'set_%s' % f.name)(rel_new_data[f.name])
                        if change and was_changed:
                            self.fields_changed.append('%s de l\'objet %s "%s"' % (f.verbose_name, rel_opts.verbose_name, new_rel_obj))

            # Si l'on se trouve en mode "changement", si tous les champs pour lesquels
            # core=True sont vierges et si la clé primaire de l'objet (son identifiant)
            # a été spécifiée, on supprime l'objet.
            if change and all_cores_blank and rel_new_data.has_key(rel_opts.pk.name) and rel_new_data[rel_opts.pk.name][0]:
                new_rel_obj.delete()
                self.fields_deleted.append('%s "%r"' % (rel_opts.verbose_name, old_rel_obj))

    # On enregistre, le cas échéant, l'ordre de tri.
    if change and opts.get_ordered_objects():
        order = new_data['order_'] and map(int, new_data['order_'].split(',')) or []
        for rel_opts in opts.get_ordered_objects():
            getattr(new_object, 'set_%s_order' % rel_opts.object_name.lower())(order)
    return new_object

def manipulator_validator_unique(f, opts, self, field_data, all_data):
    "Vérifie si la valeur du champ est unique pour ce champ."
    try:
        old_obj = opts.get_model_module().get_object(**{'%s__exact' % f.name: field_data})
    except ObjectDoesNotExist:
        return
    if hasattr(self, 'original_object') and getattr(self.original_object, opts.pk.name) == getattr(old_obj, opts.pk.name):
        return
    raise validators.ValidationError, "Un objet %s dont le champ \"%s\" est identique existe déjà." % (capfirst(opts.verbose_name), f.verbose_name)

def manipulator_validator_unique_together(field_name_list, opts, self, field_data, all_data):
    from django.utils.text import get_text_list
    field_list = [opts.get_field(field_name) for field_name in field_name_list]
    kwargs = {'%s__iexact' % field_name_list[0]: field_data}
    for f in field_list[1:]:
        field_val = all_data.get(f.name, None)
        if field_val is None:
            # Ce cas sera intercepté par un autre validateur, sauf si le champ
            # possède l'option blank=True.
            return
        kwargs['%s__iexact' % f.name] = field_val
    mod = opts.get_model_module()
    try:
        old_obj = mod.get_object(**kwargs)
    except ObjectDoesNotExist:
        return
    if hasattr(self, 'original_object') and getattr(self.original_object, opts.pk.name) == getattr(old_obj, opts.pk.name):
        pass
    else:
        raise validators.ValidationError, "Un objet %s pour lequel le champ \"%s\" a même valeur, et de champ(s) %s identique(s), existe déjà." % \
            (capfirst(opts.verbose_name), field_list[0].verbose_name, get_text_list(field_name_list[1:], 'et'))

def manipulator_validator_unique_for_date(from_field, date_field, opts, lookup_type, self, field_data, all_data):
    date_str = all_data.get(date_field.get_manipulator_field_names('')[0], None)
    mod = opts.get_model_module()
    date_val = formfields.DateField.html2python(date_str)
    if date_val is None:
        return # La date est invalide. Ce cas sera intercepté par un autre validateur.
    lookup_kwargs = {'%s__iexact' % from_field.name: field_data, '%s__year' % date_field.name: date_val.year}
    if lookup_type in ('month', 'date'):
        lookup_kwargs['%s__month' % date_field.name] = date_val.month
    if lookup_type == 'date':
        lookup_kwargs['%s__day' % date_field.name] = date_val.day
    try:
        old_obj = mod.get_object(**lookup_kwargs)
    except ObjectDoesNotExist:
        return
    else:
        if hasattr(self, 'original_object') and getattr(self.original_object, opts.pk.name) == getattr(old_obj, opts.pk.name):
            pass
        else:
            format_string = (lookup_type == 'date') and '%d %B %Y' or '%B %Y'
            raise validators.ValidationError, "Veuillez entrer une valeur différente pour le champ \"%s\". Celle que vous avez entrée est déjà utilisée pour la date \"%s\"." % \
                (from_field.verbose_name, date_val.strftime(format_string))

def manipulator_valid_rel_key(f, self, field_data, all_data):
    "Vérifie si la valeur du champ est une clé étrangère valide."
    mod = f.rel.to.get_model_module()
    try:
        mod.get_object(**{'id__iexact': field_data})
    except ObjectDoesNotExist:
        raise validators.ValidationError, "Veuillez entrer une valeur valide pour le champ \"%s\"." % f.verbose_name

####################
# CHAMPS           #
####################

class Field(object):

    # Désigne si les chaînes vides sont autorisées au niveau de la base
    # de données.
    empty_strings_allowed = True

    def __init__(self, name, verbose_name=None, primary_key=False,
        maxlength=None, unique=False, blank=False, null=False, db_index=None,
        core=False, rel=None, default=NOT_PROVIDED, editable=True,
        prepopulate_from=None, unique_for_date=None, unique_for_month=None,
        unique_for_year=None, validator_list=None, choices=None, radio_admin=None,
        help_text=''):
        self.name = name
        self.verbose_name = verbose_name or name.replace('_', ' ')
        self.primary_key = primary_key
        self.maxlength, self.unique = maxlength, unique
        self.blank, self.null = blank, null
        self.core, self.rel, self.default = core, rel, default
        self.editable = editable
        self.validator_list = validator_list or []
        self.prepopulate_from = prepopulate_from
        self.unique_for_date, self.unique_for_month = unique_for_date, unique_for_month
        self.unique_for_year = unique_for_year
        self.choices = choices or []
        self.radio_admin = radio_admin
        self.help_text = help_text
        if rel and isinstance(rel, ManyToMany):
            self.help_text += ' Maintenez enfoncée la touche "Ctrl" (ou "Command" sous Mac) pour sélectionner plus d\'un objet.'

        # On affecte True à db_index si le champ actuel est impliqué dans une relation et qu'il ne définit pas explicitement db_index.
        if db_index is None:
            if isinstance(rel, OneToOne) or isinstance(rel, ManyToOne):
                self.db_index = True
            else:
                self.db_index = False
        else:
            self.db_index = db_index

    def pre_save(self, obj, value, add):
        """
        Un hook permettant de modifier l'objet obj en fonction de la valeur du champ
        actuel (argument value) et du flag ajout/modification (argument add).
        """
        pass

    def get_db_prep_save(self, value, add):
        "Prépare la valeur du champ de manière à ce qu'elle puisse être enregistrée à l'intérieur de la base de données, et renvoie le résultat de cette préparation."
        return value

    def get_db_prep_lookup(self, lookup_type, value):
        "Prépare la valeur du champ de manière à ce que l'on puisse effectuer une recherche dessus, et renvoie le résultat de cette préparation."
        if lookup_type in ('exact', 'gt', 'gte', 'lt', 'lte', 'ne', 'month', 'day'):
            return [value]
        elif lookup_type == 'range':
            return value
        elif lookup_type == 'year':
            return ['%s-01-01' % value, '%s-12-31' % value]
        elif lookup_type in ('contains', 'icontains'):
            return ["%%%s%%" % prep_for_like_query(value)]
        elif lookup_type == 'iexact':
            return [prep_for_like_query(value)]
        elif lookup_type == 'startswith':
            return ["%s%%" % prep_for_like_query(value)]
        elif lookup_type == 'endswith':
            return ["%%%s" % prep_for_like_query(value)]
        elif lookup_type == 'isnull':
            return []
        raise TypeError, "Un champ s'est vu associer un type de recherche invalide : %s" % lookup_type

    def get_m2m_db_table(self, original_opts):
        "Renvoie le nom de la table de base de données qui servira à implémenter la relation dans laquelle est impliquée le champ actuel."
        return '%s_%s' % (original_opts.db_table, self.name)

    def has_default(self):
        "Renvoie un booléen qui indique si le champ actuel possède une valeur par défaut."
        return self.default != NOT_PROVIDED

    def get_default(self):
        "Renvoie la valeur par défaut du champ actuel."
        if self.default != NOT_PROVIDED:
            if hasattr(self.default, '__get_value__'):
                return self.default.__get_value__()
            return self.default
        if self.null:
            return None
        return ""

    def get_manipulator_field_names(self, name_prefix):
        """
        Renvoie une liste des noms de champ que l'objet actuel ajoute au manipulateur.
        """
        return [name_prefix + self.name]

    def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False):
        """
        Renvoie une liste d'instances formfields.FormField correspondant au champ
        actuel. Les valeurs possibles pour le champ actuel sont déterminées au
        moment de l'exécution et pas au moment de la compilation.

        L'argument name_prefix est un préfixe à ajouter à l'argument "field_name".
        L'argument rel est un booléen qui indique si le champ actuel est impliqué
        dans une relation.
        """
        params = {'validator_list': self.validator_list[:]}
        if self.maxlength and not self.choices: # On ne donne pas de paramètre maxlength aux champs SelectField.
            params['maxlength'] = self.maxlength
        if isinstance(self.rel, ManyToOne):
            if self.rel.raw_id_admin:
                field_objs = self.get_manipulator_field_objs()
                params['validator_list'].append(curry(manipulator_valid_rel_key, self, manipulator))
            else:
                if self.radio_admin:
                    field_objs = [formfields.RadioSelectField]
                    params['choices'] = self.get_choices(include_blank=self.blank, blank_choice=BLANK_CHOICE_NONE)
                    params['ul_class'] = get_ul_class(self.radio_admin)
                else:
                    if self.null:
                        field_objs = [formfields.NullSelectField]
                    else:
                        field_objs = [formfields.SelectField]
                    params['choices'] = self.get_choices()
        elif self.choices:
            if self.radio_admin:
                field_objs = [formfields.RadioSelectField]
                params['choices'] = self.get_choices(include_blank=self.blank, blank_choice=BLANK_CHOICE_NONE)
                params['ul_class'] = get_ul_class(self.radio_admin)
            else:
                field_objs = [formfields.SelectField]
                params['choices'] = self.get_choices()
        else:
            field_objs = self.get_manipulator_field_objs()

        # On ajoute un ou plusieurs validateurs vérifiant l'unicité du champ actuel lorsque considéré avec d'autres champs.
        for field_name_list in opts.unique_together:
            if field_name_list[0] == self.name:
                params['validator_list'].append(getattr(manipulator, 'isUnique%s' % '_'.join(field_name_list)))

        # On ajoute un ou plusieurs validateurs vérifiant l'unicité du champ actuel par rapport à d'autres champs.
        if self.unique_for_date:
            params['validator_list'].append(getattr(manipulator, 'isUnique%sFor%s' % (self.name, self.unique_for_date)))
        if self.unique_for_month:
            params['validator_list'].append(getattr(manipulator, 'isUnique%sFor%s' % (self.name, self.unique_for_month)))
        if self.unique_for_year:
            params['validator_list'].append(getattr(manipulator, 'isUnique%sFor%s' % (self.name, self.unique_for_year)))
        if self.unique:
            params['validator_list'].append(curry(manipulator_validator_unique, self, opts, manipulator))

        # On ajoute is_required=True si et seulement si le champ actuel ne peut pas
        # être laissé vierge. Les clés primaires constituent des cas particuliers, et
        # les champs impliqués dans une relation doivent affecter False à is_required
        # car le fait de les laisser vierges sera traité par un validateur à part (le
        # validateur RequiredIfOtherFieldGiven, si vous voulez tout savoir).
        params['is_required'] = not self.blank and not self.primary_key and not rel

        # Si le champ actuel est impliqué dans une relation, on vérifie si d'autres
        # champs concernés par la relation définissent core=True. Si c'est le cas, on
        # ajoute un validateur (RequiredIfOtherFieldsGiven) à l'objet FormField.
        if rel and not self.blank and not isinstance(self, AutoField) and not isinstance(self, FileField):
            # D'abord, on obtient les champs pour lesquels core=True, s'il y en a.
            core_field_names = []
            for f in opts.fields:
                if f.core and f != self:
                    core_field_names.extend(f.get_manipulator_field_names(name_prefix))
            # Si de tels champs existent, on ajoute le validateur à l'objet FormField.
            if core_field_names:
                params['validator_list'].append(validators.RequiredIfOtherFieldsGiven(core_field_names, "Ce champ est obligatoire."))

        # Les champs BooleanField (CheckboxField) constituent des cas particuliers.
        # Les paramètres is_required ou validator_list n'ont pas de signification pour
        # eux.
        if isinstance(self, BooleanField):
            del params['validator_list'], params['is_required']

        # Finalement, on obtient les noms de champ.
        field_names = self.get_manipulator_field_names(name_prefix)
        return [man(field_name=field_names[i], **params) for i, man in enumerate(field_objs)]

    def get_manipulator_new_data(self, new_data, rel=False):
        """
        À partir du dictionnaire new_data complet (obtenu auprès du manipulateur), renvoie
        les données du champ actuel.
        """
        if rel:
            return new_data.get(self.name, [self.get_default()])[0]
        else:
            val = new_data.get(self.name, self.get_default())
            if not self.empty_strings_allowed and val == '' and self.null:
                val = None
            return val

    def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH):
        "Renvoie une liste des tuples représentant les valeurs possibles pour le champ actuel."
        first_choice = include_blank and blank_choice or []
        if self.choices:
            return first_choice + list(self.choices)
        rel_obj = self.rel.to
        return first_choice + [(getattr(x, rel_obj.pk.name), repr(x)) for x in rel_obj.get_model_module().get_list(**self.rel.limit_choices_to)]

class AutoField(Field):
    empty_strings_allowed = False
    def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False):
        if not rel:
            return [] # On n'ajoute pas d'objet FormField sauf si le champ actuel est impliqué dans une relation.
        return Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel)

    def get_manipulator_field_objs(self):
        return [formfields.HiddenField]

    def get_manipulator_new_data(self, new_data, rel=False):
        if not rel:
            return None
        return Field.get_manipulator_new_data(self, new_data, rel)

class BooleanField(Field):
    def __init__(self, *args, **kwargs):
        kwargs['blank'] = True
        Field.__init__(self, *args, **kwargs)

    def get_manipulator_field_objs(self):
        return [formfields.CheckboxField]

class CharField(Field):
    def get_manipulator_field_objs(self):
        return [formfields.TextField]

class CommaSeparatedIntegerField(CharField):
    def get_manipulator_field_objs(self):
        return [formfields.CommaSeparatedIntegerField]

class DateField(Field):
    empty_strings_allowed = False
    def __init__(self, name, verbose_name=None, auto_now=False, auto_now_add=False, **kwargs):
        self.auto_now, self.auto_now_add = auto_now, auto_now_add
        if auto_now or auto_now_add:
            kwargs['editable'] = False
        Field.__init__(self, name, verbose_name, **kwargs)

    def get_db_prep_lookup(self, lookup_type, value):
        if lookup_type == 'range':
            value = [str(v) for v in value]
        else:
            value = str(value)
        return Field.get_db_prep_lookup(self, lookup_type, value)

    def pre_save(self, obj, value, add):
        if self.auto_now or (self.auto_now_add and add):
            setattr(obj, self.name, datetime.datetime.now())

    def get_db_prep_save(self, value, add):
        # On transforme les dates en chaînes avant entrée dans la base de données.
        if value is not None:
            value = value.strftime('%Y-%m-%d')
        return Field.get_db_prep_save(self, value, add)

    def get_manipulator_field_objs(self):
        return [formfields.DateField]

class DateTimeField(DateField):
    def get_db_prep_save(self, value, add):
        # On transforme les dates en chaînes avant entrée dans la base de données.
        if value is not None:
            value = value.strftime('%Y-%m-%d %H:%M:%S')
        return Field.get_db_prep_save(self, value, add)

    def get_manipulator_field_objs(self):
        return [formfields.DateField, formfields.TimeField]

    def get_manipulator_field_names(self, name_prefix):
        return [name_prefix + self.name + '_date', name_prefix + self.name + '_time']

    def get_manipulator_new_data(self, new_data, rel=False):
        date_field, time_field = self.get_manipulator_field_names('')
        if rel:
            d = new_data.get(date_field, [None])[0]
            t = new_data.get(time_field, [None])[0]
        else:
            d = new_data.get(date_field, None)
            t = new_data.get(time_field, None)
        if d is not None and t is not None:
            return datetime.datetime.combine(d, t)
        return self.get_default()

class EmailField(Field):
    def get_manipulator_field_objs(self):
        return [formfields.EmailField]

class FileField(Field):
    def __init__(self, name, verbose_name=None, upload_to='', **kwargs):
        self.upload_to = upload_to
        Field.__init__(self, name, verbose_name, **kwargs)

    def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False):
        field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel)

        if not self.blank:
            if rel:
                # Ce validateur nous permet de nous assurer que les champs FileField fonctionnent lorsqu'ils sont impliqués dans une relation.
                class RequiredFileField:
                    def __init__(self, other_field_names, other_file_field_name):
                        self.other_field_names = other_field_names
                        self.other_file_field_name = other_file_field_name
                        self.always_test = True
                    def __call__(self, field_data, all_data):
                        if not all_data.get(self.other_file_field_name, False):
                            c = validators.RequiredIfOtherFieldsGiven(self.other_field_names, "Ce champ est obligatoire.")
                            c(field_data, all_data)
                # D'abord, on obtient les champs pour lesquels core=True, s'il y en a.
                core_field_names = []
                for f in opts.fields:
                    if f.core and f != self:
                        core_field_names.extend(f.get_manipulator_field_names(name_prefix))
                # Si de tels champs existent, on ajoute le validateur à l'objet FormField.
                if core_field_names:
                    field_list[0].validator_list.append(RequiredFileField(core_field_names, field_list[1].field_name))
            else:
                v = validators.RequiredIfOtherFieldNotGiven(field_list[1].field_name, "Ce champ est obligatoire.")
                v.always_test = True
                field_list[0].validator_list.append(v)
                field_list[0].is_required = field_list[1].is_required = False

        # Passez à cette fonction le chemin d'accès brut du fichier et elle vérifiera que le fichier se trouve en-deça du paramètre MEDIA_ROOT.
        def isWithinMediaRoot(field_data, all_data):
            f = os.path.abspath(os.path.join(settings.MEDIA_ROOT, field_data))
            if not f.startswith(os.path.normpath(settings.MEDIA_ROOT)):
                raise validators.ValidationError, "Entrez un nom de fichier valide."
        field_list[1].validator_list.append(isWithinMediaRoot)
        return field_list

    def get_manipulator_field_objs(self):
        return [formfields.FileUploadField, formfields.HiddenField]

    def get_manipulator_field_names(self, name_prefix):
        return [name_prefix + self.name + '_file', name_prefix + self.name]

    def save_file(self, new_data, new_object, original_object, change, rel):
        upload_field_name = self.get_manipulator_field_names('')[0]
        if new_data.get(upload_field_name, False):
            if rel:
                getattr(new_object, 'save_%s_file' % self.name)(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0]["content"])
            else:
                getattr(new_object, 'save_%s_file' % self.name)(new_data[upload_field_name]["filename"], new_data[upload_field_name]["content"])

    def get_directory_name(self):
        return os.path.normpath(datetime.datetime.now().strftime(self.upload_to))

    def get_filename(self, filename):
        from django.utils.text import get_valid_filename
        f = os.path.join(self.get_directory_name(), get_valid_filename(os.path.basename(filename)))
        return os.path.normpath(f)

class FloatField(Field):
    empty_strings_allowed = False
    def __init__(self, name, verbose_name=None, max_digits=None, decimal_places=None, **kwargs):
        self.max_digits, self.decimal_places = max_digits, decimal_places
        Field.__init__(self, name, verbose_name, **kwargs)

    def get_manipulator_field_objs(self):
        return [curry(formfields.FloatField, max_digits=self.max_digits, decimal_places=self.decimal_places)]

class ImageField(FileField):
    def __init__(self, name, verbose_name=None, width_field=None, height_field=None, **kwargs):
        self.width_field, self.height_field = width_field, height_field
        FileField.__init__(self, name, verbose_name, **kwargs)

    def get_manipulator_field_objs(self):
        return [formfields.ImageUploadField, formfields.HiddenField]

    def save_file(self, new_data, new_object, original_object, change, rel):
        FileField.save_file(self, new_data, new_object, original_object, change, rel)
        # Si le champ ImageField actuel est associé à un champ destiné à recevoir
        # la hauteur de l'image et/ou à un champ destiné à recevoir la longueur de
        # l'image, et s'il n'y a pas eu de changement, on affecte aux champs
        # destinés à recevoir longueur/hauteur de l'image leur valeur d'origine.
        if change and (self.width_field or self.height_field):
            if self.width_field:
                setattr(new_object, self.width_field, getattr(original_object, self.width_field))
            if self.height_field:
                setattr(new_object, self.height_field, getattr(original_object, self.height_field))
            new_object.save()

class IntegerField(Field):
    empty_strings_allowed = False
    def get_manipulator_field_objs(self):
        return [formfields.IntegerField]

class IPAddressField(Field):
    def __init__(self, *args, **kwargs):
        kwargs['maxlength'] = 15
        Field.__init__(self, *args, **kwargs)

    def get_manipulator_field_objs(self):
        return [formfields.IPAddressField]

class NullBooleanField(Field):
    def __init__(self, *args, **kwargs):
        kwargs['null'] = True
        Field.__init__(self, *args, **kwargs)

    def get_manipulator_field_objs(self):
        return [formfields.NullBooleanField]

class PhoneNumberField(IntegerField):
    def get_manipulator_field_objs(self):
        return [formfields.PhoneNumberField]

class PositiveIntegerField(IntegerField):
    def get_manipulator_field_objs(self):
        return [formfields.PositiveIntegerField]

class PositiveSmallIntegerField(IntegerField):
    def get_manipulator_field_objs(self):
        return [formfields.PositiveSmallIntegerField]

class SlugField(Field):
    def __init__(self, *args, **kwargs):
        kwargs['maxlength'] = 50
        kwargs.setdefault('validator_list', []).append(validators.isAlphaNumeric)
        # On définit db_index=True sauf si db_index a été défini à la main.
        if not kwargs.has_key('db_index'):
            kwargs['db_index'] = True
        Field.__init__(self, *args, **kwargs)

    def get_manipulator_field_objs(self):
        return [formfields.TextField]

class SmallIntegerField(IntegerField):
    def get_manipulator_field_objs(self):
        return [formfields.SmallIntegerField]

class TextField(Field):
    def get_manipulator_field_objs(self):
        return [formfields.LargeTextField]

class TimeField(Field):
    empty_strings_allowed = False
    def __init__(self, name, verbose_name=None, auto_now=False, auto_now_add=False, **kwargs):
        self.auto_now, self.auto_now_add  = auto_now, auto_now_add
        if auto_now or auto_now_add:
            kwargs['editable'] = False
        Field.__init__(self, name, verbose_name, **kwargs)

    def get_db_prep_lookup(self, lookup_type, value):
        if lookup_type == 'range':
            value = [str(v) for v in value]
        else:
            value = str(value)
        return Field.get_db_prep_lookup(self, lookup_type, value)

    def pre_save(self, obj, value, add):
        if self.auto_now or (self.auto_now_add and add):
            setattr(obj, self.name, datetime.datetime.now().time())

    def get_db_prep_save(self, value, add):
        # On transforme les dates en chaînes avant entrée dans la base de données.
        if value is not None:
            value = value.strftime('%H:%M:%S')
        return Field.get_db_prep_save(self, value, add)

    def get_manipulator_field_objs(self):
        return [formfields.TimeField]

class URLField(Field):
    def __init__(self, name, verbose_name=None, verify_exists=True, **kwargs):
        if verify_exists:
            kwargs.setdefault('validator_list', []).append(validators.isExistingURL)
        Field.__init__(self, name, verbose_name, **kwargs)

    def get_manipulator_field_objs(self):
        return [formfields.URLField]

class USStateField(Field):
    def get_manipulator_field_objs(self):
        return [formfields.USStateField]

class XMLField(Field):
    def __init__(self, name, verbose_name=None, schema_path=None, **kwargs):
        self.schema_path = schema_path
        Field.__init__(self, name, verbose_name, **kwargs)

    def get_manipulator_field_objs(self):
        return [curry(formfields.XMLLargeTextField, schema_path=self.schema_path)]

class ForeignKey(Field):
    empty_strings_allowed = False
    def __init__(self, to, to_field=None, rel_name=None, **kwargs):
        try:
            to_name = to._meta.object_name.lower()
        except AttributeError: # to._meta n'existe pas, to doit donc prendre la valeur RECURSIVE_RELATIONSHIP_CONSTANT
            assert to == 'self', "Le champ ForeignKey(%r) est invalide. Le premier paramètre passé au constructeur de la classe ForeignKey doit être soit un modèle de données soit la chaîne %r" % (to, RECURSIVE_RELATIONSHIP_CONSTANT)
            kwargs['name'] = ''
            kwargs['verbose_name'] = kwargs.get('verbose_name', '')
        else:
            to_field = to_field or to._meta.pk.name
            kwargs['name'] = kwargs.get('name', to_name + '_id')
            kwargs['verbose_name'] = kwargs.get('verbose_name', to._meta.verbose_name)
            rel_name = rel_name or to_name
        kwargs['rel'] = ManyToOne(to, rel_name, to_field,
            num_in_admin=kwargs.pop('num_in_admin', 0),
            min_num_in_admin=kwargs.pop('min_num_in_admin', None),
            max_num_in_admin=kwargs.pop('max_num_in_admin', None),
            num_extra_on_change=kwargs.pop('num_extra_on_change', 1),
            edit_inline=kwargs.pop('edit_inline', False),
            edit_inline_type=kwargs.pop('edit_inline_type', STACKED),
            related_name=kwargs.pop('related_name', None),
            limit_choices_to=kwargs.pop('limit_choices_to', None),
            lookup_overrides=kwargs.pop('lookup_overrides', None),
            raw_id_admin=kwargs.pop('raw_id_admin', False))
        Field.__init__(self, **kwargs)

    def get_manipulator_field_objs(self):
        return [formfields.IntegerField]

class ManyToManyField(Field):
    def __init__(self, to, **kwargs):
        kwargs['name'] = kwargs.get('name', to._meta.module_name)
        kwargs['verbose_name'] = kwargs.get('verbose_name', to._meta.verbose_name_plural)
        kwargs['rel'] = ManyToMany(to, to._meta.object_name.lower(),
            num_in_admin=kwargs.pop('num_in_admin', 0),
            related_name=kwargs.pop('related_name', None),
            filter_interface=kwargs.pop('filter_interface', None),
            limit_choices_to=kwargs.pop('limit_choices_to', None))
        Field.__init__(self, **kwargs)

    def get_manipulator_field_objs(self):
        choices = self.get_choices(include_blank=False)
        return [curry(formfields.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)]

class OneToOneField(IntegerField):
    def __init__(self, to, to_field=None, rel_name=None, **kwargs):
        kwargs['name'] = kwargs.get('name', 'id')
        kwargs['verbose_name'] = kwargs.get('verbose_name', 'ID')
        to_field = to_field or to._meta.pk.name
        rel_name = rel_name or to._meta.object_name.lower()
        kwargs['rel'] = OneToOne(to, rel_name, to_field,
            num_in_admin=kwargs.pop('num_in_admin', 0),
            edit_inline=kwargs.pop('edit_inline', False),
            edit_inline_type=kwargs.pop('edit_inline_type', STACKED),
            related_name=kwargs.pop('related_name', None),
            limit_choices_to=kwargs.pop('limit_choices_to', None),
            lookup_overrides=kwargs.pop('lookup_overrides', None),
            raw_id_admin=kwargs.pop('raw_id_admin', False))
        kwargs['primary_key'] = True
        IntegerField.__init__(self, **kwargs)

#############
# RELATIONS #
#############

class ManyToOne:
    def __init__(self, to, name, field_name, num_in_admin=0, min_num_in_admin=None,
        max_num_in_admin=None, num_extra_on_change=1, edit_inline=False, edit_inline_type=STACKED,
        related_name=None, limit_choices_to=None, lookup_overrides=None, raw_id_admin=False):
        try:
            self.to = to._meta
        except AttributeError: # to._meta n'existe pas, to doit donc prendre la valeur RECURSIVE_RELATIONSHIP_CONSTANT
            assert to == RECURSIVE_RELATIONSHIP_CONSTANT, "'to' doit être soit un modèle de données, soit la chaîne '%s'" % RECURSIVE_RELATIONSHIP_CONSTANT
            self.to = to
        self.name, self.field_name = name, field_name
        self.num_in_admin, self.edit_inline = num_in_admin, edit_inline
        self.min_num_in_admin, self.max_num_in_admin = min_num_in_admin, max_num_in_admin
        self.num_extra_on_change = num_extra_on_change
        self.edit_inline_type, self.related_name = edit_inline_type, related_name
        self.limit_choices_to = limit_choices_to or {}
        self.lookup_overrides = lookup_overrides or {}
        self.raw_id_admin = raw_id_admin

    def get_cache_name(self):
        return '_%s_cache' % self.name

    def get_related_field(self):
        "Renvoie l'objet Field de l'objet 'to' qui correspond à l'autre bout de la relation actuelle."
        return self.to.get_field(self.field_name)

class ManyToMany:
    def __init__(self, to, name, num_in_admin=0, related_name=None,
        filter_interface=None, limit_choices_to=None):
        self.to, self.name = to._meta, name
        self.num_in_admin = num_in_admin
        self.related_name = related_name
        self.filter_interface = filter_interface
        self.limit_choices_to = limit_choices_to or {}
        self.edit_inline = False

class OneToOne(ManyToOne):
    def __init__(self, to, name, field_name, num_in_admin=0, edit_inline=False,
        edit_inline_type=STACKED, related_name=None, limit_choices_to=None, lookup_overrides=None,
        raw_id_admin=False):
        self.to, self.name, self.field_name = to._meta, name, field_name
        self.num_in_admin, self.edit_inline = num_in_admin, edit_inline
        self.edit_inline_type, self.related_name = edit_inline_type, related_name
        self.limit_choices_to = limit_choices_to or {}
        self.lookup_overrides = lookup_overrides or {}
        self.raw_id_admin = raw_id_admin

class Admin:
    def __init__(self, fields, js=None, list_display=None, list_filter=None, date_hierarchy=None,
        save_as=False, ordering=None, search_fields=None, save_on_top=False):
        self.fields = fields
        self.js = js or []
        self.list_display = list_display or ['__repr__']
        self.list_filter = list_filter or []
        self.date_hierarchy = date_hierarchy
        self.save_as, self.ordering = save_as, ordering
        self.search_fields = search_fields or []
        self.save_on_top = save_on_top

    def get_field_objs(self, opts):
        # Renvoie self.fields avec les champs sous forme d'objets Field; self.fields fait normalement
        # référence aux champs par leur nom, pas par l'objet Field qui les représente.
        new_fieldset_list = []
        for fieldset in self.fields:
            new_fieldset = [fieldset[0], {}]
            new_fieldset[1].update(fieldset[1])
            admin_fields = []
            for field_name_or_list in fieldset[1]['fields']:
                if isinstance(field_name_or_list, basestring):
                    admin_fields.append([opts.get_field(field_name_or_list)])
                else:
                    admin_fields.append([opts.get_field(field_name) for field_name in field_name_or_list])
            new_fieldset[1]['fields'] = admin_fields
            new_fieldset_list.append(new_fieldset)
        return new_fieldset_list